mirror of
https://github.com/zadam/trilium.git
synced 2026-09-02 03:38:20 +02:00
Merge branch 'develop' into feat/add-rootless-dockerfiles
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
TRILIUM_ENV=production
|
||||
TRILIUM_DATA_DIR=./apps/server/data
|
||||
TRILIUM_DATA_DIR=./apps/server/data
|
||||
TRILIUM_PORT=8082
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22.15.0-bullseye-slim AS builder
|
||||
FROM node:22.15.1-bullseye-slim AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ FROM node:22.15.0-bullseye-slim AS builder
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:22.15.0-bullseye-slim
|
||||
FROM node:22.15.1-bullseye-slim
|
||||
# Install only runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22.15.0-alpine AS builder
|
||||
FROM node:22.15.1-alpine AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ FROM node:22.15.0-alpine AS builder
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:22.15.0-alpine
|
||||
FROM node:22.15.1-alpine
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache su-exec shadow
|
||||
|
||||
|
||||
@@ -4,14 +4,10 @@
|
||||
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"better-sqlite3": "11.10.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
"jquery-hotkeys": "0.2.2",
|
||||
"@highlightjs/cdn-assets": "11.11.1"
|
||||
"better-sqlite3": "11.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/remote": "2.1.2",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cls-hooked": "4.3.9",
|
||||
@@ -42,13 +38,8 @@
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/xml2js": "0.4.14",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"boxicons": "2.1.4",
|
||||
"express-http-proxy": "2.1.1",
|
||||
"jquery": "3.7.1",
|
||||
"katex": "0.16.22",
|
||||
"normalize.css": "8.0.1",
|
||||
"@anthropic-ai/sdk": "0.50.4",
|
||||
"@anthropic-ai/sdk": "0.51.0",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/express-partial-content": "workspace:*",
|
||||
@@ -68,7 +59,7 @@
|
||||
"debounce": "2.2.0",
|
||||
"debug": "4.4.1",
|
||||
"ejs": "3.1.10",
|
||||
"electron": "36.2.0",
|
||||
"electron": "36.2.1",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
@@ -83,28 +74,27 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.1.2",
|
||||
"i18next": "25.2.0",
|
||||
"i18next-fs-backend": "2.6.0",
|
||||
"image-type": "5.2.0",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "5.0.0",
|
||||
"is-animated": "2.0.2",
|
||||
"is-svg": "6.0.0",
|
||||
"jimp": "1.6.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "26.1.0",
|
||||
"marked": "15.0.11",
|
||||
"marked": "15.0.12",
|
||||
"mime-types": "3.0.1",
|
||||
"multer": "1.4.5-lts.2",
|
||||
"multer": "2.0.0",
|
||||
"normalize-strings": "1.1.1",
|
||||
"ollama": "0.5.15",
|
||||
"openai": "4.98.0",
|
||||
"openai": "4.100.0",
|
||||
"rand-token": "1.0.1",
|
||||
"safe-compare": "1.1.4",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.16.0",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sax": "1.4.1",
|
||||
"serve-favicon": "2.5.0",
|
||||
"session-file-store": "1.5.0",
|
||||
"stream-throttle": "0.1.3",
|
||||
"strip-bom": "5.0.0",
|
||||
"striptags": "3.2.0",
|
||||
@@ -115,7 +105,7 @@
|
||||
"tmp": "0.2.3",
|
||||
"turndown": "7.2.0",
|
||||
"unescape": "1.0.1",
|
||||
"webpack": "5.99.8",
|
||||
"webpack": "5.99.9",
|
||||
"ws": "8.18.2",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "3.2.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeAll } from "vitest";
|
||||
import i18next from "i18next";
|
||||
import { join } from "path";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
beforeAll(async () => {
|
||||
// Initialize the translations manually to avoid any side effects.
|
||||
@@ -15,4 +16,8 @@ beforeAll(async () => {
|
||||
loadPath: join(__dirname, "../src/assets/translations/{{lng}}/{{ns}}.json")
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize dayjs
|
||||
await import("dayjs/locale/en.js");
|
||||
dayjs.locale("en");
|
||||
});
|
||||
|
||||
@@ -4,9 +4,8 @@ import favicon from "serve-favicon";
|
||||
import cookieParser from "cookie-parser";
|
||||
import helmet from "helmet";
|
||||
import compression from "compression";
|
||||
import sessionParser from "./routes/session_parser.js";
|
||||
import config from "./services/config.js";
|
||||
import utils, { getResourceDir } from "./services/utils.js";
|
||||
import utils, { getResourceDir, isDev } from "./services/utils.js";
|
||||
import assets from "./routes/assets.js";
|
||||
import routes from "./routes/routes.js";
|
||||
import custom from "./routes/custom.js";
|
||||
@@ -64,7 +63,7 @@ export default async function buildApp() {
|
||||
console.log("Database not initialized yet. LLM features will be initialized after setup.");
|
||||
}
|
||||
|
||||
const publicDir = path.join(getResourceDir(), "public");
|
||||
const publicDir = isDev ? path.join(getResourceDir(), "../dist/public") : path.join(getResourceDir(), "public");
|
||||
const publicAssetsDir = path.join(publicDir, "assets");
|
||||
const assetsDir = RESOURCE_DIR;
|
||||
|
||||
@@ -111,6 +110,8 @@ export default async function buildApp() {
|
||||
app.use(`/manifest.webmanifest`, express.static(path.join(publicAssetsDir, "manifest.webmanifest")));
|
||||
app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt")));
|
||||
app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png")));
|
||||
|
||||
const sessionParser = (await import("./routes/session_parser.js")).default;
|
||||
app.use(sessionParser);
|
||||
app.use(favicon(path.join(assetsDir, "icon.ico")));
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT,
|
||||
expires INTEGER
|
||||
);
|
||||
@@ -187,3 +187,9 @@ CREATE TABLE IF NOT EXISTS "embedding_providers" (
|
||||
"dateModified" TEXT NOT NULL,
|
||||
"utcDateModified" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT,
|
||||
expires INTEGER
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<p><strong>Note: This feature has not been merged yet, so it is not available.</strong>
|
||||
</p>
|
||||
<p>Multi-factor authentication (MFA) is a security process that requires
|
||||
users to provide two or more verification factors to gain access to a system,
|
||||
application, or account. This adds an extra layer of protection beyond
|
||||
@@ -7,80 +5,60 @@
|
||||
<p>By requiring more than one verification method, MFA helps reduce the risk
|
||||
of unauthorized access, even if someone has obtained your password. It’s
|
||||
highly recommended for securing sensitive information stored in your notes.</p>
|
||||
<p>Warning! OpenID and TOTP cannot be both used at the same time!</p>
|
||||
<h2>Log in with your Google Account with OpenID!</h2>
|
||||
<p>OpenID is a standardized way to let you log into websites using an account
|
||||
from another service, like Google, to verify your identity.</p>
|
||||
<h2>Why Time-based One Time Passwords?</h2>
|
||||
<p>TOTP (Time-Based One-Time Password) is a security feature that generates
|
||||
a unique, temporary code on your device, like a smartphone, which changes
|
||||
every 30 seconds. You use this code, along with your password, to log into
|
||||
your account, making it much harder for anyone else to access them.</p>
|
||||
<h2>Setup</h2>
|
||||
<h3>TOTP</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Start Trilium Notes normally.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Go to "Menu" -> "Options" -> "MFA"</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Click the "Generate TOTP Secret" button</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Copy the generated secret to your authentication app/extension</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Set an environment variable "TOTP_SECRET" as the generated secret. Environment
|
||||
variables can be set with a .env file in the root directory, by defining
|
||||
them in the command line, or with a docker container.</p><pre><code class="language-text-x-trilium-auto"># .env in the project root directory
|
||||
TOTP_ENABLED="true"
|
||||
TOTP_SECRET="secret"</code></pre><pre><code class="language-text-x-trilium-auto"># Terminal/CLI
|
||||
export TOTP_ENABLED="true"
|
||||
export TOTP_SECRET="secret"</code></pre><pre><code class="language-text-x-trilium-auto"># Docker
|
||||
docker run -p 8080:8080 -v ~/trilium-data:/home/node/trilium-data -e TOTP_ENABLED="true" -e TOTP_SECRET="secret" triliumnext/notes:[VERSION]</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Restart Trilium</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Go to "Options" -> "MFA"</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Click the "Generate Recovery Codes" button</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Save the recovery codes. Recovery codes can be used once in place of the
|
||||
<aside
|
||||
class="admonition warning">
|
||||
<p>OpenID and TOTP cannot be both used at the same time!</p>
|
||||
</aside>
|
||||
<h2>Log in with your Google Account with OpenID!</h2>
|
||||
<p>OpenID is a standardized way to let you log into websites using an account
|
||||
from another service, like Google, to verify your identity.</p>
|
||||
<h2>Why Time-based One Time Passwords?</h2>
|
||||
<p>TOTP (Time-Based One-Time Password) is a security feature that generates
|
||||
a unique, temporary code on your device, like a smartphone, which changes
|
||||
every 30 seconds. You use this code, along with your password, to log into
|
||||
your account, making it much harder for anyone else to access them.</p>
|
||||
<h2>Setup</h2>
|
||||
<p>MFA can only be set up on a server instance.</p>
|
||||
<aside class="admonition note">
|
||||
<p>When Multi-Factor Authentication (MFA) is enabled on a server instance,
|
||||
a new desktop instance may fail to sync with it. As a temporary workaround,
|
||||
you can disable MFA to complete the initial sync, then re-enable MFA afterward.
|
||||
This issue will be addressed in a future release.</p>
|
||||
</aside>
|
||||
<h3>TOTP</h3>
|
||||
<ol>
|
||||
<li>Go to "Menu" -> "Options" -> "MFA"</li>
|
||||
<li>Click the “Enable Multi-Factor Authentication” checkbox if not checked</li>
|
||||
<li>Choose “Time-Based One-Time Password (TOTP)” under MFA Method</li>
|
||||
<li>Click the "Generate TOTP Secret" button</li>
|
||||
<li>Copy the generated secret to your authentication app/extension</li>
|
||||
<li>Click the "Generate Recovery Codes" button</li>
|
||||
<li>Save the recovery codes. Recovery codes can be used once in place of the
|
||||
TOTP if you loose access to your authenticator. After a rerecovery code
|
||||
is used, it will show the unix timestamp when it was used in the MFA options
|
||||
tab.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Load the secret into an authentication app like google authenticator</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>OpenID</h3>
|
||||
<p><em>Currently only compatible with Google. Other services like Authentik and Auth0 are planned on being added.</em>
|
||||
</p>
|
||||
<p>In order to setup OpenID, you will need to setup a authentication provider.
|
||||
This requires a bit of extra setup. Follow <a href="https://developers.google.com/identity/openid-connect/openid-connect">these instructions</a> to
|
||||
setup an OpenID service through google.</p>
|
||||
<p>Set an environment variable "SSO_ENABLED" to true and add the client ID
|
||||
and secret you obtained from google. Environment variables can be set with
|
||||
a .env file in the root directory, by defining them in the command line,
|
||||
or with a docker container.</p>
|
||||
<h4>.env File</h4><pre><code class="language-text-x-trilium-auto"># .env in the project root directory
|
||||
SSO_ENABLED="true"
|
||||
BASE_URL="http://localhost:8080"
|
||||
CLIENT_ID=
|
||||
SECRET=</code></pre>
|
||||
<h4>Environment variable (linux)</h4><pre><code class="language-text-x-trilium-auto">export SSO_ENABLED="true"
|
||||
export BASE_URL="http://localhost:8080"
|
||||
export CLIENT_ID=
|
||||
export SECRET=</code></pre>
|
||||
<h4>Docker</h4><pre><code class="language-text-x-trilium-auto">docker run -d -p 8080:8080 -v ~/trilium-data:/home/node/trilium-data -e SSO_ENABLED="true" -e BASE_URL="http://localhost:8080" -e CLIENT_ID= -e SECRET= triliumnext/notes:[VERSION]</code></pre>
|
||||
<p>After you restart Trilium Notes, you will be redirected to Google's account
|
||||
selection page. Login to an account and Trilium Next will bind to that
|
||||
account, allowing you to login with it.</p>
|
||||
<p>You can now login using your google account.</p>
|
||||
tab.</li>
|
||||
<li>Re-login will be required after TOTP setup is finished (After you refreshing
|
||||
the page).</li>
|
||||
</ol>
|
||||
<h3>OpenID</h3>
|
||||
<aside class="admonition note">
|
||||
<p>Currently only compatible with Google. Other services like Authentik and
|
||||
Auth0 are planned on being added.</p>
|
||||
</aside>
|
||||
<p>In order to setup OpenID, you will need to setup a authentication provider.
|
||||
This requires a bit of extra setup. Follow <a href="https://developers.google.com/identity/openid-connect/openid-connect">these instructions</a> to
|
||||
setup an OpenID service through google.</p>
|
||||
<ol>
|
||||
<li>Set the <code>oauthBaseUrl</code>, <code>oauthClientId</code> and <code>oauthClientSecret</code> in
|
||||
the <code>config.ini</code> file (check <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a> for
|
||||
more information).
|
||||
<ol>
|
||||
<li>You can also setup through environment variables (<code>TRILIUM_OAUTH_BASE_URL</code>, <code>TRILIUM_OAUTH_CLIENT_ID</code> and <code>TRILIUM_OAUTH_CLIENT_SECRET</code>).</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>Restart the server</li>
|
||||
<li>Go to "Menu" -> "Options" -> "MFA"</li>
|
||||
<li>Click the “Enable Multi-Factor Authentication” checkbox if not checked</li>
|
||||
<li>Choose “OAuth/OpenID” under MFA Method</li>
|
||||
<li>Refresh the page and login through OpenID provider</li>
|
||||
</ol>
|
||||
@@ -40,7 +40,7 @@
|
||||
<h2>Color schemes</h2>
|
||||
<p>Since Trilium 0.94.0 the colors of code notes can be customized by going
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_4TIF1oA4VQRO">Options</a> → Code Notes and looking for the <em>Appearance</em> section.</p>
|
||||
class="reference-link" href="#root/_help_4TIF1oA4VQRO">Options</a> → Code Notes and looking for the <em>Appearance</em> section.</p>
|
||||
<aside
|
||||
class="admonition note">
|
||||
<p><strong>Why are there only a few themes whereas the code block themes for text notes have a lot?</strong>
|
||||
|
||||
@@ -193,11 +193,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "搜索:"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "无语法高亮",
|
||||
"theme_group_light": "浅色主题",
|
||||
"theme_group_dark": "深色主题"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "同步服务器主机未配置。请先配置同步。",
|
||||
"successful": "同步服务器握手成功,同步已开始。"
|
||||
|
||||
@@ -185,11 +185,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "Suche:"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "Keine Syntax-Hervorhebung",
|
||||
"theme_group_light": "Helle Themen",
|
||||
"theme_group_dark": "Dunkle Themen"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "Der Synchronisations-Server-Host ist nicht konfiguriert. Bitte konfiguriere zuerst die Synchronisation.",
|
||||
"successful": "Die Server-Verbindung wurde erfolgreich hergestellt, die Synchronisation wurde gestartet."
|
||||
|
||||
@@ -193,11 +193,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "Search:"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "No syntax highlighting",
|
||||
"theme_group_light": "Light themes",
|
||||
"theme_group_dark": "Dark themes"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "Sync server host is not configured. Please configure sync first.",
|
||||
"successful": "Sync server handshake has been successful, sync has been started."
|
||||
|
||||
@@ -189,11 +189,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "Buscar:"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "Sin resaltado de sintaxis",
|
||||
"theme_group_light": "Temas claros",
|
||||
"theme_group_dark": "Temas oscuros"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "El servidor de sincronización no está configurado. Por favor configure primero la sincronización.",
|
||||
"successful": "El protocolo de enlace del servidor de sincronización ha sido exitoso, la sincronización ha comenzado."
|
||||
|
||||
@@ -189,11 +189,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "Recherche :"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "Pas de coloration syntaxique",
|
||||
"theme_group_light": "Thèmes clairs",
|
||||
"theme_group_dark": "Thèmes sombres"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "L'hôte du serveur de synchronisation n'est pas configuré. Veuillez d'abord configurer la synchronisation.",
|
||||
"successful": "L'établissement de liaison du serveur de synchronisation a été réussi, la synchronisation a été démarrée."
|
||||
|
||||
@@ -186,11 +186,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "Pesquisar:"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "Sem destaque de sintaxe",
|
||||
"theme_group_light": "Temas claros",
|
||||
"theme_group_dark": "Temas escuros"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "O host do servidor de sincronização não está configurado. Por favor, configure a sincronização primeiro.",
|
||||
"successful": "A comunicação com o servidor de sincronização foi bem-sucedida, a sincronização foi iniciada."
|
||||
|
||||
@@ -189,11 +189,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "Căutare:"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "Fără evidențiere de sintaxă",
|
||||
"theme_group_dark": "Teme întunecate",
|
||||
"theme_group_light": "Teme luminoase"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "Calea către serverul de sincronizare nu este configurată. Configurați sincronizarea înainte.",
|
||||
"successful": "Comunicarea cu serverul de sincronizare a avut loc cu succes, s-a început sincronizarea."
|
||||
|
||||
@@ -185,11 +185,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "搜尋:"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "無格式高亮",
|
||||
"theme_group_light": "淺色主題",
|
||||
"theme_group_dark": "深色主題"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "並未設定同步伺服器主機,請先設定同步",
|
||||
"successful": "成功與同步伺服器握手,現在開始同步"
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
|
||||
<link rel="stylesheet" href="<%= appPath %>/desktop.css">
|
||||
<title>TriliumNext Notes</title>
|
||||
</head>
|
||||
<body class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>">
|
||||
@@ -34,17 +32,7 @@
|
||||
<!-- Required for correct loading of scripts in Electron -->
|
||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||
|
||||
<script src="<%= assetPath %>/node_modules/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
|
||||
<!-- Include Fancytree library and skip -->
|
||||
<link href="<%= assetPath %>/stylesheets/tree.css" rel="stylesheet">
|
||||
<script src="<%= assetPath %>/node_modules/jquery.fancytree/dist/jquery.fancytree-all-deps.min.js"></script>
|
||||
|
||||
<script src="<%= assetPath %>/node_modules/jquery-hotkeys/jquery-hotkeys.js"></script>
|
||||
|
||||
<script src="<%= assetPath %>/node_modules/autocomplete.js/dist/autocomplete.jquery.min.js"></script>
|
||||
|
||||
<link href="<%= appPath %>/bootstrap.css" rel="stylesheet">
|
||||
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
|
||||
<link href="api/fonts" rel="stylesheet">
|
||||
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
|
||||
@@ -64,14 +52,8 @@
|
||||
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
|
||||
<link href="<%= assetPath %>/stylesheets/print.css" rel="stylesheet" media="print">
|
||||
|
||||
<script>
|
||||
$("body").show();
|
||||
</script>
|
||||
|
||||
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
|
||||
<script src="<%= appPath %>/desktop.js" crossorigin type="module"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="<%= assetPath %>/node_modules/boxicons/css/boxicons.min.css">
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
</style>
|
||||
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
|
||||
<link rel="stylesheet" href="<%= appPath %>/login.css">
|
||||
<link rel="stylesheet" href="<%= appPath %>/bootstrap.css">
|
||||
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-light.css">
|
||||
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next.css">
|
||||
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/style.css">
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<title>TriliumNext Notes</title>
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
|
||||
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
|
||||
<link rel="stylesheet" href="<%= appPath %>/mobile.css">
|
||||
|
||||
<style>
|
||||
.lds-roller {
|
||||
display: inline-block;
|
||||
@@ -111,17 +108,11 @@
|
||||
|
||||
<%- include("./partials/windowGlobal.ejs", locals) %>
|
||||
|
||||
<script src="<%= assetPath %>/node_modules/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="<%= assetPath %>/node_modules/autocomplete.js/dist/autocomplete.jquery.min.js"></script>
|
||||
|
||||
<link href="<%= assetPath %>/stylesheets/tree.css" rel="stylesheet">
|
||||
<script src="<%= assetPath %>/node_modules/jquery.fancytree/dist/jquery.fancytree-all-deps.min.js"></script>
|
||||
|
||||
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
|
||||
<script src="<%= appPath %>/mobile.js" crossorigin type="module"></script>
|
||||
|
||||
<link href="api/fonts" rel="stylesheet">
|
||||
<link rel="stylesheet" href="<%= appPath %>/bootstrap.css">
|
||||
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
|
||||
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
|
||||
<% if (themeCssUrl) { %>
|
||||
@@ -130,7 +121,5 @@
|
||||
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
|
||||
<link href="<%= assetPath %>/stylesheets/print.css" rel="stylesheet" media="print">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="<%= assetPath %>/node_modules/boxicons/css/boxicons.min.css">
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= assetPath %>/images/app-icons/ios/apple-touch-icon.png">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
|
||||
<link rel="stylesheet" href="<%= appPath %>/set_password.css">
|
||||
<link rel="stylesheet" href="<%= appPath %>/bootstrap.css">
|
||||
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-light.css">
|
||||
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next.css">
|
||||
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/style.css">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<title><%= t("setup.title") %></title>
|
||||
|
||||
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
|
||||
<link rel="stylesheet" href="<%= appPath %>/setup.css">
|
||||
<link rel="stylesheet" href="<%= appPath %>/bootstrap.css">
|
||||
|
||||
<style>
|
||||
.lds-ring {
|
||||
@@ -168,9 +168,6 @@
|
||||
<!-- Required for correct loading of scripts in Electron -->
|
||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||
|
||||
<script src="<%= assetPath %>/node_modules/jquery/dist/jquery.min.js"></script>
|
||||
<script src="<%= assetPath %>/node_modules/jquery-hotkeys/jquery-hotkeys.js"></script>
|
||||
|
||||
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
|
||||
<script src="<%= appPath %>/setup.js" crossorigin type="module"></script>
|
||||
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet" />
|
||||
|
||||
@@ -12,13 +12,9 @@
|
||||
<% } else { %>
|
||||
<link rel="shortcut icon" href="../favicon.ico">
|
||||
<% } %>
|
||||
<script src="../<%= appPath %>/share.js"></script>
|
||||
<script src="<%= appPath %>/share.js" type="module"></script>
|
||||
<% if (!note.isLabelTruthy("shareOmitDefaultCss")) { %>
|
||||
<link href="../<%= assetPath %>/node_modules/normalize.css/normalize.css" rel="stylesheet">
|
||||
<link href="../<%= assetPath %>/stylesheets/share.css" rel="stylesheet">
|
||||
<% } %>
|
||||
<% if (note.type === 'text' || note.type === 'book') { %>
|
||||
<link href="../<%= assetPath %>/libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
|
||||
<link href="<%= assetPath %>/stylesheets/share.css" rel="stylesheet">
|
||||
<% } %>
|
||||
<% for (const cssRelation of note.getRelations("shareCss")) { %>
|
||||
<link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">
|
||||
|
||||
@@ -101,7 +101,7 @@ async function importNotesToBranch(req: Request) {
|
||||
return note.getPojo();
|
||||
}
|
||||
|
||||
async function importAttachmentsToNote(req: Request) {
|
||||
function importAttachmentsToNote(req: Request) {
|
||||
const { parentNoteId } = req.params;
|
||||
const { taskId, last } = req.body;
|
||||
|
||||
@@ -121,7 +121,7 @@ async function importAttachmentsToNote(req: Request) {
|
||||
// unlike in note import, we let the events run, because a huge number of attachments is not likely
|
||||
|
||||
try {
|
||||
await singleImportService.importAttachment(taskContext, file, parentNote);
|
||||
singleImportService.importAttachment(taskContext, file, parentNote);
|
||||
} catch (e: unknown) {
|
||||
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import searchService from "../../services/search/services/search.js";
|
||||
import ValidationError from "../../errors/validation_error.js";
|
||||
import type { Request } from "express";
|
||||
import { changeLanguage, getLocales } from "../../services/i18n.js";
|
||||
import { listSyntaxHighlightingThemes } from "../../services/code_block_theme.js";
|
||||
import type { OptionNames } from "@triliumnext/commons";
|
||||
|
||||
// options allowed to be updated directly in the Options dialog
|
||||
@@ -190,10 +189,6 @@ function getUserThemes() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getSyntaxHighlightingThemes() {
|
||||
return listSyntaxHighlightingThemes();
|
||||
}
|
||||
|
||||
function getSupportedLocales() {
|
||||
return getLocales();
|
||||
}
|
||||
@@ -210,6 +205,5 @@ export default {
|
||||
updateOption,
|
||||
updateOptions,
|
||||
getUserThemes,
|
||||
getSyntaxHighlightingThemes,
|
||||
getSupportedLocales
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import assetPath from "../services/asset_path.js";
|
||||
import { assetUrlFragment } from "../services/asset_path.js";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import express from "express";
|
||||
import { getResourceDir, isDev } from "../services/utils.js";
|
||||
import type serveStatic from "serve-static";
|
||||
import proxy from "express-http-proxy";
|
||||
import contentCss from "@triliumnext/ckeditor5/content.css";
|
||||
|
||||
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
|
||||
if (!isDev) {
|
||||
@@ -21,77 +20,29 @@ async function register(app: express.Application) {
|
||||
const srcRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const resourceDir = getResourceDir();
|
||||
|
||||
app.use(`/${assetPath}/libraries/ckeditor/ckeditor-content.css`, (req, res) => res.contentType("text/css").send(contentCss));
|
||||
|
||||
if (isDev) {
|
||||
const publicUrl = process.env.TRILIUM_PUBLIC_SERVER;
|
||||
if (!publicUrl) {
|
||||
throw new Error("Missing TRILIUM_PUBLIC_SERVER");
|
||||
}
|
||||
|
||||
const clientProxy = proxy(publicUrl);
|
||||
app.use(`/${assetPath}/app/doc_notes`, persistentCacheStatic(path.join(srcRoot, "assets", "doc_notes")));
|
||||
app.use(`/${assetPath}/app`, clientProxy);
|
||||
app.use(`/${assetPath}/app-dist`, clientProxy);
|
||||
app.use(`/${assetPath}/stylesheets`, proxy(publicUrl, {
|
||||
proxyReqPathResolver: (req) => "/stylesheets" + req.url
|
||||
app.use("/" + assetUrlFragment + `/@fs`, proxy(publicUrl, {
|
||||
proxyReqPathResolver: (req) => "/" + assetUrlFragment + `/@fs` + req.url
|
||||
}));
|
||||
app.use(`/${assetPath}/libraries`, proxy(publicUrl, {
|
||||
proxyReqPathResolver: (req) => "/libraries" + req.url
|
||||
}));
|
||||
app.use(`/${assetPath}/fonts`, proxy(publicUrl, {
|
||||
proxyReqPathResolver: (req) => "/fonts" + req.url
|
||||
}));
|
||||
app.use(`/${assetPath}/translations`, proxy(publicUrl, {
|
||||
proxyReqPathResolver: (req) => "/translations" + req.url
|
||||
}));
|
||||
app.use(`/${assetPath}/images`, persistentCacheStatic(path.join(srcRoot, "assets", "images")));
|
||||
} else {
|
||||
const clientStaticCache = persistentCacheStatic(path.join(resourceDir, "public"));
|
||||
app.use(`/${assetPath}/app`, clientStaticCache);
|
||||
app.use(`/${assetPath}/app-dist`, clientStaticCache);
|
||||
app.use(`/${assetPath}/stylesheets`, persistentCacheStatic(path.join(resourceDir, "public", "stylesheets")));
|
||||
app.use(`/${assetPath}/libraries`, persistentCacheStatic(path.join(resourceDir, "public", "libraries")));
|
||||
app.use(`/${assetPath}/fonts`, persistentCacheStatic(path.join(resourceDir, "public", "fonts")));
|
||||
app.use(`/${assetPath}/translations/`, persistentCacheStatic(path.join(resourceDir, "public", "translations")));
|
||||
app.use(`/${assetPath}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images")));
|
||||
app.use(`/${assetPath}/app-dist/doc_notes`, persistentCacheStatic(path.join(resourceDir, "assets", "doc_notes")));
|
||||
app.use(`/${assetPath}/app-dist/excalidraw/fonts`, persistentCacheStatic(path.join(resourceDir, "node_modules/@excalidraw/excalidraw/dist/prod/fonts")));
|
||||
app.use(`/${assetUrlFragment}/src`, persistentCacheStatic(path.join(resourceDir, "public", "src")));
|
||||
app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(resourceDir, "public", "stylesheets")));
|
||||
app.use(`/${assetUrlFragment}/libraries`, persistentCacheStatic(path.join(resourceDir, "public", "libraries")));
|
||||
app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(resourceDir, "public", "fonts")));
|
||||
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(resourceDir, "public", "translations")));
|
||||
app.use(`/${assetUrlFragment}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images")));
|
||||
app.use(`/node_modules/`, persistentCacheStatic(path.join(resourceDir, "public/node_modules")));
|
||||
}
|
||||
app.use(`/${assetUrlFragment}/doc_notes`, persistentCacheStatic(path.join(resourceDir, "assets", "doc_notes")));
|
||||
app.use(`/assets/vX/fonts`, express.static(path.join(srcRoot, "public/fonts")));
|
||||
app.use(`/assets/vX/images`, express.static(path.join(srcRoot, "..", "images")));
|
||||
app.use(`/assets/vX/stylesheets`, express.static(path.join(srcRoot, "public/stylesheets")));
|
||||
app.use(`/${assetPath}/libraries`, persistentCacheStatic(path.join(srcRoot, "public/libraries")));
|
||||
app.use(`/${assetUrlFragment}/libraries`, persistentCacheStatic(path.join(srcRoot, "public/libraries")));
|
||||
app.use(`/assets/vX/libraries`, express.static(path.join(srcRoot, "..", "libraries")));
|
||||
|
||||
const nodeModulesDir = isDev ? path.join(srcRoot, "..", "node_modules") : path.join(resourceDir, "node_modules");
|
||||
|
||||
app.use(`/node_modules/@excalidraw/excalidraw/dist/fonts/`, express.static(path.join(nodeModulesDir, "@excalidraw/excalidraw/dist/prod/fonts/")));
|
||||
app.use(`/${assetPath}/node_modules/@excalidraw/excalidraw/dist/fonts/`, persistentCacheStatic(path.join(nodeModulesDir, "@excalidraw/excalidraw/dist/prod/fonts/")));
|
||||
|
||||
// KaTeX
|
||||
app.use(`/${assetPath}/node_modules/katex/dist/katex.min.js`, persistentCacheStatic(path.join(nodeModulesDir, "katex/dist/katex.min.js")));
|
||||
app.use(`/${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js`, persistentCacheStatic(path.join(nodeModulesDir, "katex/dist/contrib/mhchem.min.js")));
|
||||
app.use(`/${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js`, persistentCacheStatic(path.join(nodeModulesDir, "katex/dist/contrib/auto-render.min.js")));
|
||||
// expose the whole dist folder
|
||||
app.use(`/node_modules/katex/dist/`, express.static(path.join(nodeModulesDir, "katex/dist/")));
|
||||
app.use(`/${assetPath}/node_modules/katex/dist/`, persistentCacheStatic(path.join(nodeModulesDir, "katex/dist/")));
|
||||
|
||||
app.use(`/${assetPath}/node_modules/boxicons/css/`, persistentCacheStatic(path.join(nodeModulesDir, "boxicons/css/")));
|
||||
app.use(`/${assetPath}/node_modules/boxicons/fonts/`, persistentCacheStatic(path.join(nodeModulesDir, "boxicons/fonts/")));
|
||||
|
||||
app.use(`/${assetPath}/node_modules/jquery/dist/`, persistentCacheStatic(path.join(nodeModulesDir, "jquery/dist/")));
|
||||
|
||||
app.use(`/${assetPath}/node_modules/jquery-hotkeys/`, persistentCacheStatic(path.join(nodeModulesDir, "jquery-hotkeys/")));
|
||||
|
||||
// Deprecated, https://www.npmjs.com/package/autocomplete.js?activeTab=readme
|
||||
app.use(`/${assetPath}/node_modules/autocomplete.js/dist/`, persistentCacheStatic(path.join(nodeModulesDir, "autocomplete.js/dist/")));
|
||||
|
||||
app.use(`/${assetPath}/node_modules/normalize.css/`, persistentCacheStatic(path.join(nodeModulesDir, "normalize.css/")));
|
||||
|
||||
app.use(`/${assetPath}/node_modules/jquery.fancytree/dist/`, persistentCacheStatic(path.join(nodeModulesDir, "jquery.fancytree/dist/")));
|
||||
|
||||
app.use(`/${assetPath}/node_modules/@highlightjs/cdn-assets/`, persistentCacheStatic(path.join(nodeModulesDir, "@highlightjs/cdn-assets/")));
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -14,15 +14,12 @@ describe("Login Route test", () => {
|
||||
|
||||
it("should return the login page, when using a GET request", async () => {
|
||||
|
||||
// RegExp for login page specific string in HTML: e.g. "assets/v0.92.7/app/login.css"
|
||||
const loginCssRegexp = /assets\/v[0-9.a-z]+\/app\/login\.css/;
|
||||
|
||||
// RegExp for login page specific string in HTML
|
||||
const res = await supertest(app)
|
||||
.get("/login")
|
||||
.expect(200)
|
||||
|
||||
|
||||
expect(loginCssRegexp.test(res.text)).toBe(true);
|
||||
expect(res.text).toMatch(/assets\/v[0-9.a-z]+\/src\/login\.js/);
|
||||
|
||||
});
|
||||
|
||||
|
||||
198
apps/server/src/routes/route_api.ts
Normal file
198
apps/server/src/routes/route_api.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import express from "express";
|
||||
import multer from "multer";
|
||||
import log from "../services/log.js";
|
||||
import cls from "../services/cls.js";
|
||||
import sql from "../services/sql.js";
|
||||
import entityChangesService from "../services/entity_changes.js";
|
||||
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
|
||||
import NotFoundError from "../errors/not_found_error.js";
|
||||
import ValidationError from "../errors/validation_error.js";
|
||||
import auth from "../services/auth.js";
|
||||
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
|
||||
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||
|
||||
const MAX_ALLOWED_FILE_SIZE_MB = 250;
|
||||
export const router = express.Router();
|
||||
|
||||
// TODO: Deduplicate with etapi_utils.ts afterwards.
|
||||
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
|
||||
|
||||
export type ApiResultHandler = (req: express.Request, res: express.Response, result: unknown) => number;
|
||||
|
||||
type NotAPromise<T> = T & { then?: void };
|
||||
export type ApiRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => unknown;
|
||||
export type SyncRouteRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => NotAPromise<object> | number | string | void | null;
|
||||
|
||||
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
|
||||
function convertEntitiesToPojo(result: unknown) {
|
||||
if (result instanceof AbstractBeccaEntity) {
|
||||
result = result.getPojo();
|
||||
} else if (Array.isArray(result)) {
|
||||
for (const idx in result) {
|
||||
if (result[idx] instanceof AbstractBeccaEntity) {
|
||||
result[idx] = result[idx].getPojo();
|
||||
}
|
||||
}
|
||||
} else if (result && typeof result === "object") {
|
||||
if ("note" in result && result.note instanceof AbstractBeccaEntity) {
|
||||
result.note = result.note.getPojo();
|
||||
}
|
||||
|
||||
if ("branch" in result && result.branch instanceof AbstractBeccaEntity) {
|
||||
result.branch = result.branch.getPojo();
|
||||
}
|
||||
}
|
||||
|
||||
if (result && typeof result === "object" && "executionResult" in result) {
|
||||
// from runOnBackend()
|
||||
result.executionResult = convertEntitiesToPojo(result.executionResult);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function apiResultHandler(req: express.Request, res: express.Response, result: unknown) {
|
||||
res.setHeader("trilium-max-entity-change-id", entityChangesService.getMaxEntityChangeId());
|
||||
|
||||
result = convertEntitiesToPojo(result);
|
||||
|
||||
// if it's an array and the first element is integer, then we consider this to be [statusCode, response] format
|
||||
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
|
||||
const [statusCode, response] = result;
|
||||
|
||||
if (statusCode !== 200 && statusCode !== 201 && statusCode !== 204) {
|
||||
log.info(`${req.method} ${req.originalUrl} returned ${statusCode} with response ${JSON.stringify(response)}`);
|
||||
}
|
||||
|
||||
return send(res, statusCode, response);
|
||||
} else if (result === undefined) {
|
||||
return send(res, 204, "");
|
||||
} else {
|
||||
return send(res, 200, result);
|
||||
}
|
||||
}
|
||||
|
||||
function send(res: express.Response, statusCode: number, response: unknown) {
|
||||
if (typeof response === "string") {
|
||||
if (statusCode >= 400) {
|
||||
res.setHeader("Content-Type", "text/plain");
|
||||
}
|
||||
|
||||
res.status(statusCode).send(response);
|
||||
|
||||
return response.length;
|
||||
} else {
|
||||
const json = JSON.stringify(response);
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.status(statusCode).send(json);
|
||||
|
||||
return json.length;
|
||||
}
|
||||
}
|
||||
|
||||
export function apiRoute(method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler) {
|
||||
route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
|
||||
}
|
||||
|
||||
export function asyncApiRoute(method: HttpMethod, path: string, routeHandler: ApiRequestHandler) {
|
||||
asyncRoute(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
|
||||
}
|
||||
|
||||
export function route(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: SyncRouteRequestHandler, resultHandler: ApiResultHandler | null = null) {
|
||||
internalRoute(method, path, middleware, routeHandler, resultHandler, true);
|
||||
}
|
||||
|
||||
export function asyncRoute(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null) {
|
||||
internalRoute(method, path, middleware, routeHandler, resultHandler, false);
|
||||
}
|
||||
|
||||
function internalRoute(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null, transactional: boolean) {
|
||||
router[method](path, ...(middleware as express.Handler[]), (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
cls.namespace.bindEmitter(req);
|
||||
cls.namespace.bindEmitter(res);
|
||||
|
||||
const result = cls.init(() => {
|
||||
cls.set("componentId", req.headers["trilium-component-id"]);
|
||||
cls.set("localNowDateTime", req.headers["trilium-local-now-datetime"]);
|
||||
cls.set("hoistedNoteId", req.headers["trilium-hoisted-note-id"] || "root");
|
||||
|
||||
const cb = () => routeHandler(req, res, next);
|
||||
|
||||
return transactional ? sql.transactional(cb) : cb();
|
||||
});
|
||||
|
||||
if (!resultHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result?.then) {
|
||||
// promise
|
||||
result.then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start)).catch((e: unknown) => handleException(e, method, path, res));
|
||||
} else {
|
||||
handleResponse(resultHandler, req, res, result, start);
|
||||
}
|
||||
} catch (e) {
|
||||
handleException(e, method, path, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleResponse(resultHandler: ApiResultHandler, req: express.Request, res: express.Response, result: unknown, start: number) {
|
||||
// Skip result handling if the response has already been handled
|
||||
if ((res as any).triliumResponseHandled) {
|
||||
// Just log the request without additional processing
|
||||
log.request(req, res, Date.now() - start, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const responseLength = resultHandler(req, res, result);
|
||||
log.request(req, res, Date.now() - start, responseLength);
|
||||
}
|
||||
|
||||
function handleException(e: unknown | Error, method: HttpMethod, path: string, res: express.Response) {
|
||||
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||
|
||||
log.error(`${method} ${path} threw exception: '${errMessage}', stack: ${errStack}`);
|
||||
|
||||
const resStatusCode = (e instanceof ValidationError || e instanceof NotFoundError) ? e.statusCode : 500;
|
||||
|
||||
res.status(resStatusCode).json({
|
||||
message: errMessage
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export function createUploadMiddleware() {
|
||||
const multerOptions: multer.Options = {
|
||||
fileFilter: (req: express.Request, file, cb) => {
|
||||
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
|
||||
// See https://github.com/expressjs/multer/pull/1102.
|
||||
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
|
||||
cb(null, true);
|
||||
}
|
||||
};
|
||||
|
||||
if (!process.env.TRILIUM_NO_UPLOAD_LIMIT) {
|
||||
multerOptions.limits = {
|
||||
fileSize: MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
|
||||
};
|
||||
}
|
||||
|
||||
return multer(multerOptions).single("upload");
|
||||
}
|
||||
|
||||
const uploadMiddleware = createUploadMiddleware();
|
||||
|
||||
export const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
uploadMiddleware(req, res, function (err) {
|
||||
if (err?.code === "LIMIT_FILE_SIZE") {
|
||||
res.setHeader("Content-Type", "text/plain").status(400).send(`Cannot upload file because it excceeded max allowed file size of ${MAX_ALLOWED_FILE_SIZE_MB} MiB`);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,21 +1,13 @@
|
||||
import { isElectron, safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||
import multer from "multer";
|
||||
import log from "../services/log.js";
|
||||
import { isElectron } from "../services/utils.js";
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
|
||||
import auth from "../services/auth.js";
|
||||
import openID from '../services/open_id.js';
|
||||
import totp from './api/totp.js';
|
||||
import recoveryCodes from './api/recovery_codes.js';
|
||||
import cls from "../services/cls.js";
|
||||
import sql from "../services/sql.js";
|
||||
import entityChangesService from "../services/entity_changes.js";
|
||||
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
|
||||
import { createPartialContentHandler } from "@triliumnext/express-partial-content";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
|
||||
import NotFoundError from "../errors/not_found_error.js";
|
||||
import ValidationError from "../errors/validation_error.js";
|
||||
|
||||
// page routes
|
||||
import setupRoute from "./setup.js";
|
||||
@@ -77,33 +69,14 @@ import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
|
||||
import etapiSpecRoute from "../etapi/spec.js";
|
||||
import etapiBackupRoute from "../etapi/backup.js";
|
||||
import apiDocsRoute from "./api_docs.js";
|
||||
import { apiResultHandler, apiRoute, asyncApiRoute, asyncRoute, route, router, uploadMiddlewareWithErrorHandling } from "./route_api.js";
|
||||
|
||||
|
||||
const MAX_ALLOWED_FILE_SIZE_MB = 250;
|
||||
const GET = "get",
|
||||
PST = "post",
|
||||
PUT = "put",
|
||||
PATCH = "patch",
|
||||
DEL = "delete";
|
||||
|
||||
export type ApiResultHandler = (req: express.Request, res: express.Response, result: unknown) => number;
|
||||
export type ApiRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => unknown;
|
||||
|
||||
// TODO: Deduplicate with etapi_utils.ts afterwards.
|
||||
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
|
||||
|
||||
const uploadMiddleware = createUploadMiddleware();
|
||||
|
||||
const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
uploadMiddleware(req, res, function (err) {
|
||||
if (err?.code === "LIMIT_FILE_SIZE") {
|
||||
res.setHeader("Content-Type", "text/plain").status(400).send(`Cannot upload file because it excceeded max allowed file size of ${MAX_ALLOWED_FILE_SIZE_MB} MiB`);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function register(app: express.Application) {
|
||||
route(GET, "/", [auth.checkAuth, csrfMiddleware], indexRoute.index);
|
||||
route(GET, "/login", [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage);
|
||||
@@ -126,7 +99,7 @@ function register(app: express.Application) {
|
||||
apiRoute(GET, '/api/totp/get', totp.getSecret);
|
||||
|
||||
apiRoute(GET, '/api/oauth/status', openID.getOAuthStatus);
|
||||
apiRoute(GET, '/api/oauth/validate', openID.isTokenValid);
|
||||
asyncApiRoute(GET, '/api/oauth/validate', openID.isTokenValid);
|
||||
|
||||
apiRoute(PST, '/api/totp_recovery/set', recoveryCodes.setRecoveryCodes);
|
||||
apiRoute(PST, '/api/totp_recovery/verify', recoveryCodes.verifyRecoveryCode);
|
||||
@@ -156,7 +129,7 @@ function register(app: express.Application) {
|
||||
apiRoute(PUT, "/api/notes/:noteId/clone-after/:afterBranchId", cloningApiRoute.cloneNoteAfter);
|
||||
route(PUT, "/api/notes/:noteId/file", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], filesRoute.updateFile, apiResultHandler);
|
||||
route(GET, "/api/notes/:noteId/open", [auth.checkApiAuthOrElectron], filesRoute.openFile);
|
||||
route(
|
||||
asyncRoute(
|
||||
GET,
|
||||
"/api/notes/:noteId/open-partial",
|
||||
[auth.checkApiAuthOrElectron],
|
||||
@@ -192,7 +165,7 @@ function register(app: express.Application) {
|
||||
apiRoute(GET, "/api/attachments/:attachmentId/blob", attachmentsApiRoute.getAttachmentBlob);
|
||||
route(GET, "/api/attachments/:attachmentId/image/:filename", [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage);
|
||||
route(GET, "/api/attachments/:attachmentId/open", [auth.checkApiAuthOrElectron], filesRoute.openAttachment);
|
||||
route(
|
||||
asyncRoute(
|
||||
GET,
|
||||
"/api/attachments/:attachmentId/open-partial",
|
||||
[auth.checkApiAuthOrElectron],
|
||||
@@ -221,7 +194,7 @@ function register(app: express.Application) {
|
||||
route(GET, "/api/revisions/:revisionId/download", [auth.checkApiAuthOrElectron], revisionsApiRoute.downloadRevision);
|
||||
|
||||
route(GET, "/api/branches/:branchId/export/:type/:format/:version/:taskId", [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
|
||||
route(PST, "/api/notes/:parentNoteId/notes-import", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importNotesToBranch, apiResultHandler);
|
||||
asyncRoute(PST, "/api/notes/:parentNoteId/notes-import", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importNotesToBranch, apiResultHandler);
|
||||
route(PST, "/api/notes/:parentNoteId/attachments-import", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importAttachmentsToNote, apiResultHandler);
|
||||
|
||||
apiRoute(GET, "/api/notes/:noteId/attributes", attributesRoute.getEffectiveNoteAttributes);
|
||||
@@ -244,14 +217,13 @@ function register(app: express.Application) {
|
||||
apiRoute(PUT, "/api/options/:name/:value*", optionsApiRoute.updateOption);
|
||||
apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions);
|
||||
apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes);
|
||||
apiRoute(GET, "/api/options/codeblock-themes", optionsApiRoute.getSyntaxHighlightingThemes);
|
||||
apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales);
|
||||
|
||||
apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword);
|
||||
apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword);
|
||||
|
||||
apiRoute(PST, "/api/sync/test", syncApiRoute.testSync);
|
||||
apiRoute(PST, "/api/sync/now", syncApiRoute.syncNow);
|
||||
asyncApiRoute(PST, "/api/sync/test", syncApiRoute.testSync);
|
||||
asyncApiRoute(PST, "/api/sync/now", syncApiRoute.syncNow);
|
||||
apiRoute(PST, "/api/sync/fill-entity-changes", syncApiRoute.fillEntityChanges);
|
||||
apiRoute(PST, "/api/sync/force-full-sync", syncApiRoute.forceFullSync);
|
||||
route(GET, "/api/sync/check", [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler);
|
||||
@@ -270,10 +242,10 @@ function register(app: express.Application) {
|
||||
|
||||
// group of the services below are meant to be executed from the outside
|
||||
route(GET, "/api/setup/status", [], setupApiRoute.getStatus, apiResultHandler);
|
||||
route(PST, "/api/setup/new-document", [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler, false);
|
||||
route(PST, "/api/setup/sync-from-server", [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler, false);
|
||||
asyncRoute(PST, "/api/setup/new-document", [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/sync-from-server", [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler);
|
||||
route(GET, "/api/setup/sync-seed", [auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
|
||||
route(PST, "/api/setup/sync-seed", [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler, false);
|
||||
asyncRoute(PST, "/api/setup/sync-seed", [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler);
|
||||
|
||||
apiRoute(GET, "/api/autocomplete", autocompleteApiRoute.getAutocomplete);
|
||||
apiRoute(GET, "/api/autocomplete/notesCount", autocompleteApiRoute.getNotesCount);
|
||||
@@ -304,21 +276,21 @@ function register(app: express.Application) {
|
||||
const clipperMiddleware = isElectron ? [] : [auth.checkEtapiToken];
|
||||
|
||||
route(GET, "/api/clipper/handshake", clipperMiddleware, clipperRoute.handshake, apiResultHandler);
|
||||
route(PST, "/api/clipper/clippings", clipperMiddleware, clipperRoute.addClipping, apiResultHandler);
|
||||
route(PST, "/api/clipper/notes", clipperMiddleware, clipperRoute.createNote, apiResultHandler);
|
||||
asyncRoute(PST, "/api/clipper/clippings", clipperMiddleware, clipperRoute.addClipping, apiResultHandler);
|
||||
asyncRoute(PST, "/api/clipper/notes", clipperMiddleware, clipperRoute.createNote, apiResultHandler);
|
||||
route(PST, "/api/clipper/open/:noteId", clipperMiddleware, clipperRoute.openNote, apiResultHandler);
|
||||
route(GET, "/api/clipper/notes-by-url/:noteUrl", clipperMiddleware, clipperRoute.findNotesByUrl, apiResultHandler);
|
||||
asyncRoute(GET, "/api/clipper/notes-by-url/:noteUrl", clipperMiddleware, clipperRoute.findNotesByUrl, apiResultHandler);
|
||||
|
||||
apiRoute(GET, "/api/special-notes/inbox/:date", specialNotesRoute.getInboxNote);
|
||||
apiRoute(GET, "/api/special-notes/days/:date", specialNotesRoute.getDayNote);
|
||||
apiRoute(GET, "/api/special-notes/week-first-day/:date", specialNotesRoute.getWeekFirstDayNote);
|
||||
apiRoute(GET, "/api/special-notes/weeks/:week", specialNotesRoute.getWeekNote);
|
||||
apiRoute(GET, "/api/special-notes/months/:month", specialNotesRoute.getMonthNote);
|
||||
apiRoute(GET, "/api/special-notes/quarters/:quarter", specialNotesRoute.getQuarterNote);
|
||||
asyncApiRoute(GET, "/api/special-notes/inbox/:date", specialNotesRoute.getInboxNote);
|
||||
asyncApiRoute(GET, "/api/special-notes/days/:date", specialNotesRoute.getDayNote);
|
||||
asyncApiRoute(GET, "/api/special-notes/week-first-day/:date", specialNotesRoute.getWeekFirstDayNote);
|
||||
asyncApiRoute(GET, "/api/special-notes/weeks/:week", specialNotesRoute.getWeekNote);
|
||||
asyncApiRoute(GET, "/api/special-notes/months/:month", specialNotesRoute.getMonthNote);
|
||||
asyncApiRoute(GET, "/api/special-notes/quarters/:quarter", specialNotesRoute.getQuarterNote);
|
||||
apiRoute(GET, "/api/special-notes/years/:year", specialNotesRoute.getYearNote);
|
||||
apiRoute(GET, "/api/special-notes/notes-for-month/:month", specialNotesRoute.getDayNotesForMonth);
|
||||
apiRoute(PST, "/api/special-notes/sql-console", specialNotesRoute.createSqlConsole);
|
||||
apiRoute(PST, "/api/special-notes/save-sql-console", specialNotesRoute.saveSqlConsole);
|
||||
asyncApiRoute(PST, "/api/special-notes/save-sql-console", specialNotesRoute.saveSqlConsole);
|
||||
apiRoute(PST, "/api/special-notes/search-note", specialNotesRoute.createSearchNote);
|
||||
apiRoute(PST, "/api/special-notes/save-search-note", specialNotesRoute.saveSearchNote);
|
||||
apiRoute(PST, "/api/special-notes/launchers/:noteId/reset", specialNotesRoute.resetLauncher);
|
||||
@@ -327,25 +299,25 @@ function register(app: express.Application) {
|
||||
|
||||
apiRoute(GET, "/api/sql/schema", sqlRoute.getSchema);
|
||||
apiRoute(PST, "/api/sql/execute/:noteId", sqlRoute.execute);
|
||||
route(PST, "/api/database/anonymize/:type", [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false);
|
||||
asyncRoute(PST, "/api/database/anonymize/:type", [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler);
|
||||
apiRoute(GET, "/api/database/anonymized-databases", databaseRoute.getExistingAnonymizedDatabases);
|
||||
|
||||
if (process.env.TRILIUM_INTEGRATION_TEST === "memory") {
|
||||
route(PST, "/api/database/rebuild/", [auth.checkApiAuthOrElectron], databaseRoute.rebuildIntegrationTestDatabase, apiResultHandler, false);
|
||||
asyncRoute(PST, "/api/database/rebuild/", [auth.checkApiAuthOrElectron], databaseRoute.rebuildIntegrationTestDatabase, apiResultHandler);
|
||||
}
|
||||
|
||||
// backup requires execution outside of transaction
|
||||
route(PST, "/api/database/backup-database", [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false);
|
||||
asyncRoute(PST, "/api/database/backup-database", [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler);
|
||||
apiRoute(GET, "/api/database/backups", databaseRoute.getExistingBackups);
|
||||
|
||||
// VACUUM requires execution outside of transaction
|
||||
route(PST, "/api/database/vacuum-database", [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler, false);
|
||||
asyncRoute(PST, "/api/database/vacuum-database", [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler);
|
||||
|
||||
route(PST, "/api/database/find-and-fix-consistency-issues", [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false);
|
||||
asyncRoute(PST, "/api/database/find-and-fix-consistency-issues", [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler);
|
||||
|
||||
apiRoute(GET, "/api/database/check-integrity", databaseRoute.checkIntegrity);
|
||||
|
||||
route(PST, "/api/script/exec", [auth.checkApiAuth, csrfMiddleware], scriptRoute.exec, apiResultHandler, false);
|
||||
asyncRoute(PST, "/api/script/exec", [auth.checkApiAuth, csrfMiddleware], scriptRoute.exec, apiResultHandler);
|
||||
|
||||
apiRoute(PST, "/api/script/run/:noteId", scriptRoute.run);
|
||||
apiRoute(GET, "/api/script/startup", scriptRoute.getStartupBundles);
|
||||
@@ -355,8 +327,8 @@ function register(app: express.Application) {
|
||||
|
||||
// no CSRF since this is called from android app
|
||||
route(PST, "/api/sender/login", [loginRateLimiter], loginApiRoute.token, apiResultHandler);
|
||||
route(PST, "/api/sender/image", [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler);
|
||||
route(PST, "/api/sender/note", [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler);
|
||||
asyncRoute(PST, "/api/sender/image", [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler);
|
||||
asyncRoute(PST, "/api/sender/note", [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler);
|
||||
|
||||
apiRoute(GET, "/api/keyboard-actions", keysRoute.getKeyboardActions);
|
||||
apiRoute(GET, "/api/keyboard-shortcuts-for-notes", keysRoute.getShortcutsForNotes);
|
||||
@@ -364,8 +336,8 @@ function register(app: express.Application) {
|
||||
apiRoute(PST, "/api/relation-map", relationMapApiRoute.getRelationMap);
|
||||
apiRoute(PST, "/api/notes/erase-deleted-notes-now", notesApiRoute.eraseDeletedNotesNow);
|
||||
apiRoute(PST, "/api/notes/erase-unused-attachments-now", notesApiRoute.eraseUnusedAttachmentsNow);
|
||||
apiRoute(GET, "/api/similar-notes/:noteId", similarNotesRoute.getSimilarNotes);
|
||||
apiRoute(GET, "/api/backend-log", backendLogRoute.getBackendLog);
|
||||
asyncApiRoute(GET, "/api/similar-notes/:noteId", similarNotesRoute.getSimilarNotes);
|
||||
asyncApiRoute(GET, "/api/backend-log", backendLogRoute.getBackendLog);
|
||||
apiRoute(GET, "/api/stats/note-size/:noteId", statsRoute.getNoteSize);
|
||||
apiRoute(GET, "/api/stats/subtree-size/:noteId", statsRoute.getSubtreeSize);
|
||||
apiRoute(PST, "/api/delete-notes-preview", notesApiRoute.getDeleteNotesPreview);
|
||||
@@ -393,42 +365,42 @@ function register(app: express.Application) {
|
||||
etapiBackupRoute.register(router);
|
||||
|
||||
// LLM Chat API
|
||||
apiRoute(PST, "/api/llm/chat", llmRoute.createSession);
|
||||
apiRoute(GET, "/api/llm/chat", llmRoute.listSessions);
|
||||
apiRoute(GET, "/api/llm/chat/:sessionId", llmRoute.getSession);
|
||||
apiRoute(PATCH, "/api/llm/chat/:sessionId", llmRoute.updateSession);
|
||||
apiRoute(DEL, "/api/llm/chat/:chatNoteId", llmRoute.deleteSession);
|
||||
apiRoute(PST, "/api/llm/chat/:chatNoteId/messages", llmRoute.sendMessage);
|
||||
apiRoute(PST, "/api/llm/chat/:chatNoteId/messages/stream", llmRoute.streamMessage);
|
||||
asyncApiRoute(PST, "/api/llm/chat", llmRoute.createSession);
|
||||
asyncApiRoute(GET, "/api/llm/chat", llmRoute.listSessions);
|
||||
asyncApiRoute(GET, "/api/llm/chat/:sessionId", llmRoute.getSession);
|
||||
asyncApiRoute(PATCH, "/api/llm/chat/:sessionId", llmRoute.updateSession);
|
||||
asyncApiRoute(DEL, "/api/llm/chat/:chatNoteId", llmRoute.deleteSession);
|
||||
asyncApiRoute(PST, "/api/llm/chat/:chatNoteId/messages", llmRoute.sendMessage);
|
||||
asyncApiRoute(PST, "/api/llm/chat/:chatNoteId/messages/stream", llmRoute.streamMessage);
|
||||
|
||||
// LLM index management endpoints - reorganized for REST principles
|
||||
apiRoute(GET, "/api/llm/indexes/stats", llmRoute.getIndexStats);
|
||||
apiRoute(PST, "/api/llm/indexes", llmRoute.startIndexing); // Create index process
|
||||
apiRoute(GET, "/api/llm/indexes/failed", llmRoute.getFailedIndexes);
|
||||
apiRoute(PUT, "/api/llm/indexes/notes/:noteId", llmRoute.retryFailedIndex); // Update index for note
|
||||
apiRoute(PUT, "/api/llm/indexes/failed", llmRoute.retryAllFailedIndexes); // Update all failed indexes
|
||||
apiRoute(GET, "/api/llm/indexes/notes/similar", llmRoute.findSimilarNotes); // Get similar notes
|
||||
apiRoute(GET, "/api/llm/indexes/context", llmRoute.generateQueryContext); // Get context
|
||||
apiRoute(PST, "/api/llm/indexes/notes/:noteId", llmRoute.indexNote); // Create index for specific note
|
||||
asyncApiRoute(GET, "/api/llm/indexes/stats", llmRoute.getIndexStats);
|
||||
asyncApiRoute(PST, "/api/llm/indexes", llmRoute.startIndexing); // Create index process
|
||||
asyncApiRoute(GET, "/api/llm/indexes/failed", llmRoute.getFailedIndexes);
|
||||
asyncApiRoute(PUT, "/api/llm/indexes/notes/:noteId", llmRoute.retryFailedIndex); // Update index for note
|
||||
asyncApiRoute(PUT, "/api/llm/indexes/failed", llmRoute.retryAllFailedIndexes); // Update all failed indexes
|
||||
asyncApiRoute(GET, "/api/llm/indexes/notes/similar", llmRoute.findSimilarNotes); // Get similar notes
|
||||
asyncApiRoute(GET, "/api/llm/indexes/context", llmRoute.generateQueryContext); // Get context
|
||||
asyncApiRoute(PST, "/api/llm/indexes/notes/:noteId", llmRoute.indexNote); // Create index for specific note
|
||||
|
||||
// LLM embeddings endpoints
|
||||
apiRoute(GET, "/api/llm/embeddings/similar/:noteId", embeddingsRoute.findSimilarNotes);
|
||||
apiRoute(PST, "/api/llm/embeddings/search", embeddingsRoute.searchByText);
|
||||
apiRoute(GET, "/api/llm/embeddings/providers", embeddingsRoute.getProviders);
|
||||
apiRoute(PATCH, "/api/llm/embeddings/providers/:providerId", embeddingsRoute.updateProvider);
|
||||
apiRoute(PST, "/api/llm/embeddings/reprocess", embeddingsRoute.reprocessAllNotes);
|
||||
apiRoute(GET, "/api/llm/embeddings/queue-status", embeddingsRoute.getQueueStatus);
|
||||
apiRoute(GET, "/api/llm/embeddings/stats", embeddingsRoute.getEmbeddingStats);
|
||||
apiRoute(GET, "/api/llm/embeddings/failed", embeddingsRoute.getFailedNotes);
|
||||
apiRoute(PST, "/api/llm/embeddings/retry/:noteId", embeddingsRoute.retryFailedNote);
|
||||
apiRoute(PST, "/api/llm/embeddings/retry-all-failed", embeddingsRoute.retryAllFailedNotes);
|
||||
apiRoute(PST, "/api/llm/embeddings/rebuild-index", embeddingsRoute.rebuildIndex);
|
||||
apiRoute(GET, "/api/llm/embeddings/index-rebuild-status", embeddingsRoute.getIndexRebuildStatus);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/similar/:noteId", embeddingsRoute.findSimilarNotes);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/search", embeddingsRoute.searchByText);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/providers", embeddingsRoute.getProviders);
|
||||
asyncApiRoute(PATCH, "/api/llm/embeddings/providers/:providerId", embeddingsRoute.updateProvider);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/reprocess", embeddingsRoute.reprocessAllNotes);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/queue-status", embeddingsRoute.getQueueStatus);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/stats", embeddingsRoute.getEmbeddingStats);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/failed", embeddingsRoute.getFailedNotes);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/retry/:noteId", embeddingsRoute.retryFailedNote);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/retry-all-failed", embeddingsRoute.retryAllFailedNotes);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/rebuild-index", embeddingsRoute.rebuildIndex);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/index-rebuild-status", embeddingsRoute.getIndexRebuildStatus);
|
||||
|
||||
// LLM provider endpoints - moved under /api/llm/providers hierarchy
|
||||
apiRoute(GET, "/api/llm/providers/ollama/models", ollamaRoute.listModels);
|
||||
apiRoute(GET, "/api/llm/providers/openai/models", openaiRoute.listModels);
|
||||
apiRoute(GET, "/api/llm/providers/anthropic/models", anthropicRoute.listModels);
|
||||
asyncApiRoute(GET, "/api/llm/providers/ollama/models", ollamaRoute.listModels);
|
||||
asyncApiRoute(GET, "/api/llm/providers/openai/models", openaiRoute.listModels);
|
||||
asyncApiRoute(GET, "/api/llm/providers/anthropic/models", anthropicRoute.listModels);
|
||||
|
||||
// API Documentation
|
||||
apiDocsRoute(app);
|
||||
@@ -436,156 +408,6 @@ function register(app: express.Application) {
|
||||
app.use("", router);
|
||||
}
|
||||
|
||||
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
|
||||
function convertEntitiesToPojo(result: unknown) {
|
||||
if (result instanceof AbstractBeccaEntity) {
|
||||
result = result.getPojo();
|
||||
} else if (Array.isArray(result)) {
|
||||
for (const idx in result) {
|
||||
if (result[idx] instanceof AbstractBeccaEntity) {
|
||||
result[idx] = result[idx].getPojo();
|
||||
}
|
||||
}
|
||||
} else if (result && typeof result === "object") {
|
||||
if ("note" in result && result.note instanceof AbstractBeccaEntity) {
|
||||
result.note = result.note.getPojo();
|
||||
}
|
||||
|
||||
if ("branch" in result && result.branch instanceof AbstractBeccaEntity) {
|
||||
result.branch = result.branch.getPojo();
|
||||
}
|
||||
}
|
||||
|
||||
if (result && typeof result === "object" && "executionResult" in result) {
|
||||
// from runOnBackend()
|
||||
result.executionResult = convertEntitiesToPojo(result.executionResult);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function apiResultHandler(req: express.Request, res: express.Response, result: unknown) {
|
||||
res.setHeader("trilium-max-entity-change-id", entityChangesService.getMaxEntityChangeId());
|
||||
|
||||
result = convertEntitiesToPojo(result);
|
||||
|
||||
// if it's an array and the first element is integer, then we consider this to be [statusCode, response] format
|
||||
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
|
||||
const [statusCode, response] = result;
|
||||
|
||||
if (statusCode !== 200 && statusCode !== 201 && statusCode !== 204) {
|
||||
log.info(`${req.method} ${req.originalUrl} returned ${statusCode} with response ${JSON.stringify(response)}`);
|
||||
}
|
||||
|
||||
return send(res, statusCode, response);
|
||||
} else if (result === undefined) {
|
||||
return send(res, 204, "");
|
||||
} else {
|
||||
return send(res, 200, result);
|
||||
}
|
||||
}
|
||||
|
||||
function send(res: express.Response, statusCode: number, response: unknown) {
|
||||
if (typeof response === "string") {
|
||||
if (statusCode >= 400) {
|
||||
res.setHeader("Content-Type", "text/plain");
|
||||
}
|
||||
|
||||
res.status(statusCode).send(response);
|
||||
|
||||
return response.length;
|
||||
} else {
|
||||
const json = JSON.stringify(response);
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.status(statusCode).send(json);
|
||||
|
||||
return json.length;
|
||||
}
|
||||
}
|
||||
|
||||
function apiRoute(method: HttpMethod, path: string, routeHandler: ApiRequestHandler) {
|
||||
route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
|
||||
}
|
||||
|
||||
function route(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null, transactional = true) {
|
||||
router[method](path, ...(middleware as express.Handler[]), (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
cls.namespace.bindEmitter(req);
|
||||
cls.namespace.bindEmitter(res);
|
||||
|
||||
const result = cls.init(() => {
|
||||
cls.set("componentId", req.headers["trilium-component-id"]);
|
||||
cls.set("localNowDateTime", req.headers["trilium-local-now-datetime"]);
|
||||
cls.set("hoistedNoteId", req.headers["trilium-hoisted-note-id"] || "root");
|
||||
|
||||
const cb = () => routeHandler(req, res, next);
|
||||
|
||||
return transactional ? sql.transactional(cb) : cb();
|
||||
});
|
||||
|
||||
if (!resultHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result?.then) {
|
||||
// promise
|
||||
result.then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start)).catch((e: unknown) => handleException(e, method, path, res));
|
||||
} else {
|
||||
handleResponse(resultHandler, req, res, result, start);
|
||||
}
|
||||
} catch (e) {
|
||||
handleException(e, method, path, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleResponse(resultHandler: ApiResultHandler, req: express.Request, res: express.Response, result: unknown, start: number) {
|
||||
// Skip result handling if the response has already been handled
|
||||
if ((res as any).triliumResponseHandled) {
|
||||
// Just log the request without additional processing
|
||||
log.request(req, res, Date.now() - start, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const responseLength = resultHandler(req, res, result);
|
||||
log.request(req, res, Date.now() - start, responseLength);
|
||||
}
|
||||
|
||||
function handleException(e: unknown | Error, method: HttpMethod, path: string, res: express.Response) {
|
||||
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||
|
||||
log.error(`${method} ${path} threw exception: '${errMessage}', stack: ${errStack}`);
|
||||
|
||||
const resStatusCode = (e instanceof ValidationError || e instanceof NotFoundError) ? e.statusCode : 500;
|
||||
|
||||
res.status(resStatusCode).json({
|
||||
message: errMessage
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function createUploadMiddleware() {
|
||||
const multerOptions: multer.Options = {
|
||||
fileFilter: (req: express.Request, file, cb) => {
|
||||
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
|
||||
// See https://github.com/expressjs/multer/pull/1102.
|
||||
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
|
||||
cb(null, true);
|
||||
}
|
||||
};
|
||||
|
||||
if (!process.env.TRILIUM_NO_UPLOAD_LIMIT) {
|
||||
multerOptions.limits = {
|
||||
fileSize: MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
|
||||
};
|
||||
}
|
||||
|
||||
return multer(multerOptions).single("upload");
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
|
||||
@@ -1,10 +1,55 @@
|
||||
import session from "express-session";
|
||||
import sessionFileStore from "session-file-store";
|
||||
import sql from "../services/sql.js";
|
||||
import session, { Store } from "express-session";
|
||||
import sessionSecret from "../services/session_secret.js";
|
||||
import dataDir from "../services/data_dir.js";
|
||||
import config from "../services/config.js";
|
||||
import log from "../services/log.js";
|
||||
|
||||
const FileStore = sessionFileStore(session);
|
||||
class SQLiteSessionStore extends Store {
|
||||
|
||||
get(sid: string, callback: (err: any, session?: session.SessionData | null) => void): void {
|
||||
try {
|
||||
const data = sql.getValue<string>(/*sql*/`SELECT data FROM sessions WHERE id = ?`, sid);
|
||||
let session = null;
|
||||
if (data) {
|
||||
session = JSON.parse(data);
|
||||
}
|
||||
return callback(null, session);
|
||||
} catch (e: unknown) {
|
||||
log.error(e);
|
||||
return callback(e);
|
||||
}
|
||||
}
|
||||
|
||||
set(id: string, session: session.SessionData, callback?: (err?: any) => void): void {
|
||||
try {
|
||||
const expires = session.cookie?.expires
|
||||
? new Date(session.cookie.expires).getTime()
|
||||
: Date.now() + 3600000; // fallback to 1 hour
|
||||
const data = JSON.stringify(session);
|
||||
|
||||
sql.upsert("sessions", "id", {
|
||||
id,
|
||||
expires,
|
||||
data
|
||||
});
|
||||
callback?.();
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
return callback?.(e);
|
||||
}
|
||||
}
|
||||
|
||||
destroy(sid: string, callback?: (err?: any) => void): void {
|
||||
try {
|
||||
sql.execute(/*sql*/`DELETE FROM sessions WHERE id = ?`, sid);
|
||||
callback?.();
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
callback?.(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const sessionParser = session({
|
||||
secret: sessionSecret,
|
||||
@@ -16,10 +61,14 @@ const sessionParser = session({
|
||||
maxAge: config.Session.cookieMaxAge * 1000 // needs value in milliseconds
|
||||
},
|
||||
name: "trilium.sid",
|
||||
store: new FileStore({
|
||||
ttl: config.Session.cookieMaxAge,
|
||||
path: `${dataDir.TRILIUM_DATA_DIR}/sessions`
|
||||
})
|
||||
store: new SQLiteSessionStore()
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
// Clean up expired sesions.
|
||||
const now = Date.now();
|
||||
const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now);
|
||||
console.log("Cleaning up expired sessions: ", result.changes);
|
||||
}, 60 * 60 * 1000);
|
||||
|
||||
export default sessionParser;
|
||||
|
||||
@@ -3,7 +3,7 @@ import build from "./build.js";
|
||||
import packageJson from "../../package.json" with { type: "json" };
|
||||
import dataDir from "./data_dir.js";
|
||||
|
||||
const APP_DB_VERSION = 230;
|
||||
const APP_DB_VERSION = 231;
|
||||
const SYNC_VERSION = 35;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import assetPath from "./asset_path.js";
|
||||
import { isDev } from "./utils.js";
|
||||
|
||||
export default isDev ? assetPath + "/app" : assetPath + "/app-dist";
|
||||
export default assetPath + "/src";
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import packageJson from "../../package.json" with { type: "json" };
|
||||
import { isDev } from "./utils";
|
||||
|
||||
export default `assets/v${packageJson.version}`;
|
||||
export const assetUrlFragment = `assets/v${packageJson.version}`;
|
||||
const assetPath = isDev ? `http://localhost:4200/${assetUrlFragment}` : assetUrlFragment;
|
||||
|
||||
export default assetPath;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readThemesFromFileSystem } from "./code_block_theme.js";
|
||||
|
||||
import themeNames from "./code_block_theme_names.json" with { type: "json" };
|
||||
import path = require("path");
|
||||
|
||||
describe("Code block theme", () => {
|
||||
it("all themes are mapped", () => {
|
||||
const themes = readThemesFromFileSystem(path.join(__dirname, "../../node_modules/@highlightjs/cdn-assets/styles"));
|
||||
|
||||
const mappedThemeNames = new Set(Object.values(themeNames));
|
||||
const unmappedThemeNames = new Set<string>();
|
||||
|
||||
for (const theme of themes) {
|
||||
if (!mappedThemeNames.has(theme.title)) {
|
||||
unmappedThemeNames.add(theme.title);
|
||||
}
|
||||
}
|
||||
|
||||
expect(unmappedThemeNames.size, `Unmapped themes: ${Array.from(unmappedThemeNames).join(", ")}`).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
/**
|
||||
* @module
|
||||
*
|
||||
* Manages the server-side functionality of the code blocks feature, mostly for obtaining the available themes for syntax highlighting.
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import themeNames from "./code_block_theme_names.json" with { type: "json" };
|
||||
import { t } from "i18next";
|
||||
import { join } from "path";
|
||||
import { isDev, isElectron, getResourceDir } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Represents a color scheme for the code block syntax highlight.
|
||||
*/
|
||||
interface ColorTheme {
|
||||
/** The ID of the color scheme which should be stored in the options. */
|
||||
val: string;
|
||||
/** A user-friendly name of the theme. The name is already localized. */
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the supported syntax highlighting themes for code blocks, in groups.
|
||||
*
|
||||
* The return value is an object where the keys represent groups in their human-readable name (e.g. "Light theme")
|
||||
* and the values are an array containing the information about every theme. There is also a special group with no
|
||||
* title (empty string) which should be displayed at the top of the listing pages, without a group.
|
||||
*
|
||||
* @returns the supported themes, grouped.
|
||||
*/
|
||||
export function listSyntaxHighlightingThemes() {
|
||||
const path = getStylesDirectory();
|
||||
const systemThemes = readThemesFromFileSystem(path);
|
||||
|
||||
return {
|
||||
"": [
|
||||
{
|
||||
val: "none",
|
||||
title: t("code_block.theme_none")
|
||||
}
|
||||
],
|
||||
...groupThemesByLightOrDark(systemThemes)
|
||||
};
|
||||
}
|
||||
|
||||
export function getStylesDirectory() {
|
||||
if (isElectron && !isDev) {
|
||||
return join(getResourceDir(), "styles");
|
||||
} else if (!isDev) {
|
||||
return join(getResourceDir(), "node_modules/@highlightjs/cdn-assets/styles");
|
||||
} else {
|
||||
return join(__dirname, "../node_modules/@highlightjs/cdn-assets/styles");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all the predefined themes by listing all minified CSSes from a given directory.
|
||||
*
|
||||
* The theme names are mapped against a known list in order to provide more descriptive names such as "Visual Studio 2015 (Dark)" instead of "vs2015".
|
||||
*
|
||||
* @param path the path to read from. Usually this is the highlight.js `styles` directory.
|
||||
* @returns the list of themes.
|
||||
*/
|
||||
export function readThemesFromFileSystem(path: string): ColorTheme[] {
|
||||
return fs
|
||||
.readdirSync(path)
|
||||
.filter((el) => el.endsWith(".min.css"))
|
||||
.map((name) => {
|
||||
const nameWithoutExtension = name.replace(".min.css", "");
|
||||
let title = nameWithoutExtension.replace(/-/g, " ");
|
||||
|
||||
if (title in themeNames) {
|
||||
title = (themeNames as Record<string, string>)[title];
|
||||
}
|
||||
|
||||
return {
|
||||
val: `default:${nameWithoutExtension}`,
|
||||
title: title
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups a list of themes by dark or light themes. This is done simply by checking whether "Dark" is present in the given theme, otherwise it's considered a light theme.
|
||||
* This generally only works if the theme has a known human-readable name (see {@link #readThemesFromFileSystem()})
|
||||
*
|
||||
* @param listOfThemes the list of themes to be grouped.
|
||||
* @returns the grouped themes by light or dark.
|
||||
*/
|
||||
function groupThemesByLightOrDark(listOfThemes: ColorTheme[]) {
|
||||
const darkThemes = [];
|
||||
const lightThemes = [];
|
||||
|
||||
for (const theme of listOfThemes) {
|
||||
if (theme.title.includes("Dark")) {
|
||||
darkThemes.push(theme);
|
||||
} else {
|
||||
lightThemes.push(theme);
|
||||
}
|
||||
}
|
||||
|
||||
const output: Record<string, ColorTheme[]> = {};
|
||||
output[t("code_block.theme_group_light")] = lightThemes;
|
||||
output[t("code_block.theme_group_dark")] = darkThemes;
|
||||
return output;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"1c light": "1C (Light)",
|
||||
"a11y dark": "a11y (Dark)",
|
||||
"a11y light": "a11y (Light)",
|
||||
"agate": "Agate (Dark)",
|
||||
"an old hope": "An Old Hope (Dark)",
|
||||
"androidstudio": "Android Studio (Dark)",
|
||||
"arduino light": "Arduino (Light)",
|
||||
"arta": "Arta (Dark)",
|
||||
"ascetic": "Ascetic (Light)",
|
||||
"atom one dark reasonable": "Atom One with ReasonML support (Dark)",
|
||||
"atom one dark": "Atom One (Dark)",
|
||||
"atom one light": "Atom One (Light)",
|
||||
"brown paper": "Brown Paper (Light)",
|
||||
"codepen embed": "CodePen Embed (Dark)",
|
||||
"color brewer": "Color Brewer (Light)",
|
||||
"cybertopia cherry": "Cybertopia Cherry (Dark)",
|
||||
"cybertopia dimmer": "Cybertopia Dimmer (Dark)",
|
||||
"cybertopia icecap": "Cybertopia Icecap (Dark)",
|
||||
"cybertopia saturated": "Cybertopia Saturated (Dark)",
|
||||
"dark": "Dark",
|
||||
"default": "Original highlight.js Theme (Light)",
|
||||
"devibeans": "devibeans (Dark)",
|
||||
"docco": "Docco (Light)",
|
||||
"far": "FAR (Dark)",
|
||||
"felipec": "FelipeC (Dark)",
|
||||
"foundation": "Foundation 4 Docs (Light)",
|
||||
"github dark dimmed": "GitHub Dimmed (Dark)",
|
||||
"github dark": "GitHub (Dark)",
|
||||
"github": "GitHub (Light)",
|
||||
"gml": "GML (Dark)",
|
||||
"googlecode": "Google Code (Light)",
|
||||
"gradient dark": "Gradient (Dark)",
|
||||
"gradient light": "Gradient (Light)",
|
||||
"grayscale": "Grayscale (Light)",
|
||||
"hybrid": "hybrid (Dark)",
|
||||
"idea": "Idea (Light)",
|
||||
"intellij light": "IntelliJ (Light)",
|
||||
"ir black": "IR Black (Dark)",
|
||||
"isbl editor dark": "ISBL Editor (Dark)",
|
||||
"isbl editor light": "ISBL Editor (Light)",
|
||||
"kimbie dark": "Kimbie (Dark)",
|
||||
"kimbie light": "Kimbie (Light)",
|
||||
"lightfair": "Lightfair (Light)",
|
||||
"lioshi": "Lioshi (Dark)",
|
||||
"magula": "Magula (Light)",
|
||||
"mono blue": "Mono Blue (Light)",
|
||||
"monokai sublime": "Monokai Sublime (Dark)",
|
||||
"monokai": "Monokai (Dark)",
|
||||
"night owl": "Night Owl (Dark)",
|
||||
"nnfx dark": "NNFX (Dark)",
|
||||
"nnfx light": "NNFX (Light)",
|
||||
"nord": "Nord (Dark)",
|
||||
"obsidian": "Obsidian (Dark)",
|
||||
"panda syntax dark": "Panda (Dark)",
|
||||
"panda syntax light": "Panda (Light)",
|
||||
"paraiso dark": "Paraiso (Dark)",
|
||||
"paraiso light": "Paraiso (Light)",
|
||||
"pojoaque": "Pojoaque (Dark)",
|
||||
"purebasic": "PureBasic (Light)",
|
||||
"qtcreator dark": "Qt Creator (Dark)",
|
||||
"qtcreator light": "Qt Creator (Light)",
|
||||
"rainbow": "Rainbow (Dark)",
|
||||
"routeros": "RouterOS Script (Light)",
|
||||
"rose pine dawn": "Rose Pine Dawn (Light)",
|
||||
"rose pine moon": "Rose Pine Moon (Dark)",
|
||||
"rose pine": "Rose Pine (Dark)",
|
||||
"school book": "School Book (Light)",
|
||||
"shades of purple": "Shades of Purple (Dark)",
|
||||
"srcery": "Srcery (Dark)",
|
||||
"stackoverflow dark": "Stack Overflow (Dark)",
|
||||
"stackoverflow light": "Stack Overflow (Light)",
|
||||
"sunburst": "Sunburst (Dark)",
|
||||
"tokyo night dark": "Tokyo Night (Dark)",
|
||||
"tokyo night light": "Tokyo Night (Light)",
|
||||
"tomorrow night blue": "Tomorrow Night Blue (Dark)",
|
||||
"tomorrow night bright": "Tomorrow Night Bright (Dark)",
|
||||
"vs": "Visual Studio (Light)",
|
||||
"vs2015": "Visual Studio 2015 (Dark)",
|
||||
"xcode": "Xcode (Light)",
|
||||
"xt256": "xt256 (Dark)"
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import attributeService from "./attributes.js";
|
||||
import cloningService from "./cloning.js";
|
||||
import dayjs from "dayjs";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import i18next from "i18next";
|
||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
|
||||
import noteService from "./notes.js";
|
||||
import optionService from "./options.js";
|
||||
@@ -16,6 +15,7 @@ import searchContext from "../services/search/search_context.js";
|
||||
import searchService from "../services/search/services/search.js";
|
||||
import sql from "./sql.js";
|
||||
import { t } from "i18next";
|
||||
import { ordinal } from "./i18n.js";
|
||||
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(quarterOfYear);
|
||||
@@ -67,24 +67,7 @@ function getTimeUnitReplacements(timeUnit: TimeUnit): string[] {
|
||||
return units.slice(0, index + 1).flatMap(unit => baseReplacements[unit]);
|
||||
}
|
||||
|
||||
async function ordinal(date: Dayjs, lng: string) {
|
||||
const localeMap: Record<string, string> = {
|
||||
cn: "zh-cn",
|
||||
tw: "zh-tw"
|
||||
};
|
||||
|
||||
const dayjsLocale = localeMap[lng] || lng;
|
||||
|
||||
try {
|
||||
await import(`dayjs/locale/${dayjsLocale}.js`);
|
||||
} catch (err) {
|
||||
console.warn(`Could not load locale ${dayjsLocale}`, err);
|
||||
}
|
||||
|
||||
return dayjs(date).locale(dayjsLocale).format("Do");
|
||||
}
|
||||
|
||||
async function getJournalNoteTitle(
|
||||
function getJournalNoteTitle(
|
||||
rootNote: BNote,
|
||||
timeUnit: TimeUnit,
|
||||
dateObj: Dayjs,
|
||||
@@ -102,7 +85,7 @@ async function getJournalNoteTitle(
|
||||
const monthName = t(MONTH_TRANSLATION_IDS[dateObj.month()]);
|
||||
const weekDay = t(WEEKDAY_TRANSLATION_IDS[dateObj.day()]);
|
||||
const numberStr = number.toString();
|
||||
const ordinalStr = await ordinal(dateObj, i18next.language);
|
||||
const ordinalStr = ordinal(dateObj);
|
||||
|
||||
const allReplacements: Record<string, string> = {
|
||||
// Common date formats
|
||||
@@ -228,7 +211,7 @@ function getQuarterNumberStr(date: Dayjs) {
|
||||
return `${date.year()}-Q${date.quarter()}`;
|
||||
}
|
||||
|
||||
async function getQuarterNote(quarterStr: string, _rootNote: BNote | null = null): Promise<BNote> {
|
||||
function getQuarterNote(quarterStr: string, _rootNote: BNote | null = null): BNote {
|
||||
const rootNote = _rootNote || getRootCalendarNote();
|
||||
|
||||
quarterStr = quarterStr.trim().substring(0, 7);
|
||||
@@ -247,7 +230,7 @@ async function getQuarterNote(quarterStr: string, _rootNote: BNote | null = null
|
||||
const quarterStartDate = dayjs().year(parseInt(yearStr)).month(firstMonth).date(1);
|
||||
|
||||
const yearNote = getYearNote(yearStr, rootNote);
|
||||
const noteTitle = await getJournalNoteTitle(
|
||||
const noteTitle = getJournalNoteTitle(
|
||||
rootNote, "quarter", quarterStartDate, quarterNumber
|
||||
);
|
||||
|
||||
@@ -269,7 +252,7 @@ async function getQuarterNote(quarterStr: string, _rootNote: BNote | null = null
|
||||
return quarterNote as unknown as BNote;
|
||||
}
|
||||
|
||||
async function getMonthNote(dateStr: string, _rootNote: BNote | null = null): Promise<BNote> {
|
||||
function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
const rootNote = _rootNote || getRootCalendarNote();
|
||||
|
||||
const monthStr = dateStr.substring(0, 7);
|
||||
@@ -286,12 +269,12 @@ async function getMonthNote(dateStr: string, _rootNote: BNote | null = null): Pr
|
||||
let monthParentNote;
|
||||
|
||||
if (rootNote.hasLabel("enableQuarterNote")) {
|
||||
monthParentNote = await getQuarterNote(getQuarterNumberStr(dayjs(dateStr)), rootNote);
|
||||
monthParentNote = getQuarterNote(getQuarterNumberStr(dayjs(dateStr)), rootNote);
|
||||
} else {
|
||||
monthParentNote = getYearNote(dateStr, rootNote);
|
||||
}
|
||||
|
||||
const noteTitle = await getJournalNoteTitle(
|
||||
const noteTitle = getJournalNoteTitle(
|
||||
rootNote, "month", dayjs(dateStr), parseInt(monthNumber)
|
||||
);
|
||||
|
||||
@@ -408,7 +391,7 @@ function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) {
|
||||
* @param _rootNote a {@link BNote} representing the calendar root, or {@code null} or not specified to use the default root calendar note.
|
||||
* @returns a Promise that resolves to the {@link BNote} corresponding to the week note.
|
||||
*/
|
||||
async function getWeekNote(weekStr: string, _rootNote: BNote | null = null): Promise<BNote | null> {
|
||||
function getWeekNote(weekStr: string, _rootNote: BNote | null = null): BNote | null {
|
||||
const rootNote = _rootNote || getRootCalendarNote();
|
||||
if (!rootNote.hasLabel("enableWeekNote")) {
|
||||
return null;
|
||||
@@ -435,10 +418,10 @@ async function getWeekNote(weekStr: string, _rootNote: BNote | null = null): Pro
|
||||
const startMonth = startDate.month();
|
||||
const endMonth = endDate.month();
|
||||
|
||||
const monthNote = await getMonthNote(startDate.format("YYYY-MM-DD"), rootNote);
|
||||
const noteTitle = await getJournalNoteTitle(rootNote, "week", startDate, weekNumber);
|
||||
const monthNote = getMonthNote(startDate.format("YYYY-MM-DD"), rootNote);
|
||||
const noteTitle = getJournalNoteTitle(rootNote, "week", startDate, weekNumber);
|
||||
|
||||
sql.transactional(async () => {
|
||||
sql.transactional(() => {
|
||||
weekNote = createNote(monthNote, noteTitle);
|
||||
|
||||
attributeService.createLabel(weekNote.noteId, WEEK_LABEL, weekStr);
|
||||
@@ -452,7 +435,7 @@ async function getWeekNote(weekStr: string, _rootNote: BNote | null = null): Pro
|
||||
|
||||
// If the week spans different months, clone the week note in the other month as well
|
||||
if (startMonth !== endMonth) {
|
||||
const secondMonthNote = await getMonthNote(endDate.format("YYYY-MM-DD"), rootNote);
|
||||
const secondMonthNote = getMonthNote(endDate.format("YYYY-MM-DD"), rootNote);
|
||||
cloningService.cloneNoteToParentNote(weekNote.noteId, secondMonthNote.noteId);
|
||||
}
|
||||
});
|
||||
@@ -460,7 +443,7 @@ async function getWeekNote(weekStr: string, _rootNote: BNote | null = null): Pro
|
||||
return weekNote as unknown as BNote;
|
||||
}
|
||||
|
||||
async function getDayNote(dateStr: string, _rootNote: BNote | null = null): Promise<BNote> {
|
||||
function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
const rootNote = _rootNote || getRootCalendarNote();
|
||||
|
||||
dateStr = dateStr.trim().substring(0, 10);
|
||||
@@ -476,13 +459,13 @@ async function getDayNote(dateStr: string, _rootNote: BNote | null = null): Prom
|
||||
let dateParentNote;
|
||||
|
||||
if (rootNote.hasLabel("enableWeekNote")) {
|
||||
dateParentNote = await getWeekNote(getWeekNumberStr(dayjs(dateStr)), rootNote);
|
||||
dateParentNote = getWeekNote(getWeekNumberStr(dayjs(dateStr)), rootNote);
|
||||
} else {
|
||||
dateParentNote = await getMonthNote(dateStr, rootNote);
|
||||
dateParentNote = getMonthNote(dateStr, rootNote);
|
||||
}
|
||||
|
||||
const dayNumber = dateStr.substring(8, 10);
|
||||
const noteTitle = await getJournalNoteTitle(
|
||||
const noteTitle = getJournalNoteTitle(
|
||||
rootNote, "day", dayjs(dateStr), parseInt(dayNumber)
|
||||
);
|
||||
|
||||
|
||||
@@ -5,20 +5,43 @@ import { join } from "path";
|
||||
import { getResourceDir } from "./utils.js";
|
||||
import hidden_subtree from "./hidden_subtree.js";
|
||||
import { LOCALES, type Locale } from "@triliumnext/commons";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
|
||||
const DAYJS_LOCALE_MAP: Record<string, string> = {
|
||||
cn: "zh-cn",
|
||||
tw: "zh-tw"
|
||||
};
|
||||
|
||||
let dayjsLocale: string;
|
||||
|
||||
export async function initializeTranslations() {
|
||||
const resourceDir = getResourceDir();
|
||||
const Backend = (await import("i18next-fs-backend")).default;
|
||||
const locale = getCurrentLanguage();
|
||||
|
||||
// Initialize translations
|
||||
await i18next.use(Backend).init({
|
||||
lng: getCurrentLanguage(),
|
||||
lng: locale,
|
||||
fallbackLng: "en",
|
||||
ns: "server",
|
||||
backend: {
|
||||
loadPath: join(resourceDir, "assets/translations/{{lng}}/{{ns}}.json")
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize dayjs locale.
|
||||
dayjsLocale = DAYJS_LOCALE_MAP[locale] ?? locale;
|
||||
try {
|
||||
await import(`dayjs/locale/${dayjsLocale}.js`);
|
||||
} catch (err) {
|
||||
console.warn(`Could not load locale ${dayjsLocale}`, err);
|
||||
}
|
||||
dayjs.locale(dayjsLocale);
|
||||
}
|
||||
|
||||
export function ordinal(date: Dayjs) {
|
||||
return dayjs(date)
|
||||
.format("Do");
|
||||
}
|
||||
|
||||
export function getLocales(): Locale[] {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
import { parse, Renderer, type Tokens } from "marked";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import importUtils from "./utils.js";
|
||||
import { getMimeTypeFromMarkdownName, MIME_TYPE_AUTO } from "@triliumnext/commons";
|
||||
import { ADMONITION_TYPE_MAPPINGS } from "../export/markdown.js";
|
||||
import utils from "../utils.js";
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
|
||||
/**
|
||||
* Keep renderer code up to date with https://github.com/markedjs/marked/blob/master/src/Renderer.ts.
|
||||
@@ -123,14 +129,6 @@ class CustomMarkdownRenderer extends Renderer {
|
||||
|
||||
}
|
||||
|
||||
const renderer = new CustomMarkdownRenderer({ async: false });
|
||||
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import importUtils from "./utils.js";
|
||||
import { getMimeTypeFromHighlightJs, MIME_TYPE_AUTO, normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js";
|
||||
import { ADMONITION_TYPE_MAPPINGS } from "../export/markdown.js";
|
||||
import utils from "../utils.js";
|
||||
|
||||
function renderToHtml(content: string, title: string) {
|
||||
// Double-escape slashes in math expression because they are otherwise consumed by the parser somewhere.
|
||||
content = content.replaceAll("\\$", "\\\\$");
|
||||
@@ -158,15 +156,17 @@ function renderToHtml(content: string, title: string) {
|
||||
|
||||
function getNormalizedMimeFromMarkdownLanguage(language: string | undefined) {
|
||||
if (language) {
|
||||
const highlightJsName = getMimeTypeFromHighlightJs(language);
|
||||
if (highlightJsName) {
|
||||
return normalizeMimeTypeForCKEditor(highlightJsName.mime);
|
||||
const mimeDefinition = getMimeTypeFromMarkdownName(language);
|
||||
if (mimeDefinition) {
|
||||
return normalizeMimeTypeForCKEditor(mimeDefinition.mime);
|
||||
}
|
||||
}
|
||||
|
||||
return MIME_TYPE_AUTO;
|
||||
}
|
||||
|
||||
const renderer = new CustomMarkdownRenderer({ async: false });
|
||||
|
||||
export default {
|
||||
renderToHtml
|
||||
};
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
// TODO: deduplicate with /src/public/app/services/mime_type_definitions.ts
|
||||
|
||||
/**
|
||||
* A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics.
|
||||
*/
|
||||
export const MIME_TYPE_AUTO = "text-x-trilium-auto";
|
||||
|
||||
export interface MimeTypeDefinition {
|
||||
default?: boolean;
|
||||
title: string;
|
||||
mime: string;
|
||||
/** The name of the language/mime type as defined by highlight.js (or one of the aliases), in order to be used for syntax highlighting such as inside code blocks. */
|
||||
highlightJs?: string;
|
||||
/** If specified, will load the corresponding highlight.js file from the `libraries/highlightjs/${id}.js` instead of `node_modules/@highlightjs/cdn-assets/languages/${id}.min.js`. */
|
||||
highlightJsSource?: "libraries";
|
||||
/** If specified, will load the corresponding highlight file from the given path instead of `node_modules`. */
|
||||
codeMirrorSource?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* For highlight.js-supported languages, see https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md.
|
||||
*/
|
||||
|
||||
export const MIME_TYPES_DICT: readonly MimeTypeDefinition[] = Object.freeze([
|
||||
{ title: "Plain text", mime: "text/plain", highlightJs: "plaintext", default: true },
|
||||
|
||||
// Keep sorted alphabetically.
|
||||
{ title: "APL", mime: "text/apl" },
|
||||
{ title: "ASN.1", mime: "text/x-ttcn-asn" },
|
||||
{ title: "ASP.NET", mime: "application/x-aspx" },
|
||||
{ title: "Asterisk", mime: "text/x-asterisk" },
|
||||
{ title: "Batch file (DOS)", mime: "application/x-bat", highlightJs: "dos", codeMirrorSource: "libraries/codemirror/batch.js" },
|
||||
{ title: "Brainfuck", mime: "text/x-brainfuck", highlightJs: "brainfuck" },
|
||||
{ title: "C", mime: "text/x-csrc", highlightJs: "c", default: true },
|
||||
{ title: "C#", mime: "text/x-csharp", highlightJs: "csharp", default: true },
|
||||
{ title: "C++", mime: "text/x-c++src", highlightJs: "cpp", default: true },
|
||||
{ title: "Clojure", mime: "text/x-clojure", highlightJs: "clojure" },
|
||||
{ title: "ClojureScript", mime: "text/x-clojurescript" },
|
||||
{ title: "Closure Stylesheets (GSS)", mime: "text/x-gss" },
|
||||
{ title: "CMake", mime: "text/x-cmake", highlightJs: "cmake" },
|
||||
{ title: "Cobol", mime: "text/x-cobol" },
|
||||
{ title: "CoffeeScript", mime: "text/coffeescript", highlightJs: "coffeescript" },
|
||||
{ title: "Common Lisp", mime: "text/x-common-lisp", highlightJs: "lisp" },
|
||||
{ title: "CQL", mime: "text/x-cassandra" },
|
||||
{ title: "Crystal", mime: "text/x-crystal", highlightJs: "crystal" },
|
||||
{ title: "CSS", mime: "text/css", highlightJs: "css", default: true },
|
||||
{ title: "Cypher", mime: "application/x-cypher-query" },
|
||||
{ title: "Cython", mime: "text/x-cython" },
|
||||
{ title: "D", mime: "text/x-d", highlightJs: "d" },
|
||||
{ title: "Dart", mime: "application/dart", highlightJs: "dart" },
|
||||
{ title: "diff", mime: "text/x-diff", highlightJs: "diff" },
|
||||
{ title: "Django", mime: "text/x-django", highlightJs: "django" },
|
||||
{ title: "Dockerfile", mime: "text/x-dockerfile", highlightJs: "dockerfile" },
|
||||
{ title: "DTD", mime: "application/xml-dtd" },
|
||||
{ title: "Dylan", mime: "text/x-dylan" },
|
||||
{ title: "EBNF", mime: "text/x-ebnf", highlightJs: "ebnf" },
|
||||
{ title: "ECL", mime: "text/x-ecl" },
|
||||
{ title: "edn", mime: "application/edn" },
|
||||
{ title: "Eiffel", mime: "text/x-eiffel" },
|
||||
{ title: "Elm", mime: "text/x-elm", highlightJs: "elm" },
|
||||
{ title: "Embedded Javascript", mime: "application/x-ejs" },
|
||||
{ title: "Embedded Ruby", mime: "application/x-erb", highlightJs: "erb" },
|
||||
{ title: "Erlang", mime: "text/x-erlang", highlightJs: "erlang" },
|
||||
{ title: "Esper", mime: "text/x-esper" },
|
||||
{ title: "F#", mime: "text/x-fsharp", highlightJs: "fsharp" },
|
||||
{ title: "Factor", mime: "text/x-factor" },
|
||||
{ title: "FCL", mime: "text/x-fcl" },
|
||||
{ title: "Forth", mime: "text/x-forth" },
|
||||
{ title: "Fortran", mime: "text/x-fortran", highlightJs: "fortran" },
|
||||
{ title: "Gas", mime: "text/x-gas" },
|
||||
{ title: "GDScript (Godot)", mime: "text/x-gdscript" },
|
||||
{ title: "Gherkin", mime: "text/x-feature", highlightJs: "gherkin" },
|
||||
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm", highlightJs: "markdown" },
|
||||
{ title: "Go", mime: "text/x-go", highlightJs: "go", default: true },
|
||||
{ title: "Groovy", mime: "text/x-groovy", highlightJs: "groovy", default: true },
|
||||
{ title: "HAML", mime: "text/x-haml", highlightJs: "haml" },
|
||||
{ title: "Haskell (Literate)", mime: "text/x-literate-haskell" },
|
||||
{ title: "Haskell", mime: "text/x-haskell", highlightJs: "haskell", default: true },
|
||||
{ title: "Haxe", mime: "text/x-haxe", highlightJs: "haxe" },
|
||||
{ title: "HTML", mime: "text/html", highlightJs: "xml", default: true },
|
||||
{ title: "HTTP", mime: "message/http", highlightJs: "http", default: true },
|
||||
{ title: "HXML", mime: "text/x-hxml" },
|
||||
{ title: "IDL", mime: "text/x-idl" },
|
||||
{ title: "Java Server Pages", mime: "application/x-jsp", highlightJs: "java" },
|
||||
{ title: "Java", mime: "text/x-java", highlightJs: "java", default: true },
|
||||
{ title: "Jinja2", mime: "text/jinja2" },
|
||||
{ title: "JS backend", mime: "application/javascript;env=backend", highlightJs: "javascript", default: true },
|
||||
{ title: "JS frontend", mime: "application/javascript;env=frontend", highlightJs: "javascript", default: true },
|
||||
{ title: "JSON-LD", mime: "application/ld+json", highlightJs: "json" },
|
||||
{ title: "JSON", mime: "application/json", highlightJs: "json", default: true },
|
||||
{ title: "JSX", mime: "text/jsx", highlightJs: "javascript" },
|
||||
{ title: "Julia", mime: "text/x-julia", highlightJs: "julia" },
|
||||
{ title: "Kotlin", mime: "text/x-kotlin", highlightJs: "kotlin", default: true },
|
||||
{ title: "LaTeX", mime: "text/x-latex", highlightJs: "latex" },
|
||||
{ title: "LESS", mime: "text/x-less", highlightJs: "less" },
|
||||
{ title: "LiveScript", mime: "text/x-livescript", highlightJs: "livescript" },
|
||||
{ title: "Lua", mime: "text/x-lua", highlightJs: "lua" },
|
||||
{ title: "MariaDB SQL", mime: "text/x-mariadb", highlightJs: "sql" },
|
||||
{ title: "Markdown", mime: "text/x-markdown", highlightJs: "markdown", default: true },
|
||||
{ title: "Mathematica", mime: "text/x-mathematica", highlightJs: "mathematica" },
|
||||
{ title: "mbox", mime: "application/mbox" },
|
||||
{ title: "MIPS Assembler", mime: "text/x-asm-mips", highlightJs: "mips" },
|
||||
{ title: "mIRC", mime: "text/mirc" },
|
||||
{ title: "Modelica", mime: "text/x-modelica" },
|
||||
{ title: "MS SQL", mime: "text/x-mssql", highlightJs: "sql" },
|
||||
{ title: "mscgen", mime: "text/x-mscgen" },
|
||||
{ title: "msgenny", mime: "text/x-msgenny" },
|
||||
{ title: "MUMPS", mime: "text/x-mumps" },
|
||||
{ title: "MySQL", mime: "text/x-mysql", highlightJs: "sql" },
|
||||
{ title: "Nix", mime: "text/x-nix", highlightJs: "nix" },
|
||||
{ title: "Nginx", mime: "text/x-nginx-conf", highlightJs: "nginx" },
|
||||
{ title: "NSIS", mime: "text/x-nsis", highlightJs: "nsis" },
|
||||
{ title: "NTriples", mime: "application/n-triples" },
|
||||
{ title: "Objective-C", mime: "text/x-objectivec", highlightJs: "objectivec" },
|
||||
{ title: "OCaml", mime: "text/x-ocaml", highlightJs: "ocaml" },
|
||||
{ title: "Octave", mime: "text/x-octave" },
|
||||
{ title: "Oz", mime: "text/x-oz" },
|
||||
{ title: "Pascal", mime: "text/x-pascal", highlightJs: "delphi" },
|
||||
{ title: "PEG.js", mime: "null" },
|
||||
{ title: "Perl", mime: "text/x-perl", default: true },
|
||||
{ title: "PGP", mime: "application/pgp" },
|
||||
{ title: "PHP", mime: "text/x-php", default: true },
|
||||
{ title: "Pig", mime: "text/x-pig" },
|
||||
{ title: "PLSQL", mime: "text/x-plsql", highlightJs: "sql" },
|
||||
{ title: "PostgreSQL", mime: "text/x-pgsql", highlightJs: "pgsql" },
|
||||
{ title: "PowerShell", mime: "application/x-powershell", highlightJs: "powershell" },
|
||||
{ title: "Properties files", mime: "text/x-properties", highlightJs: "properties" },
|
||||
{ title: "ProtoBuf", mime: "text/x-protobuf", highlightJs: "protobuf" },
|
||||
{ title: "Pug", mime: "text/x-pug" },
|
||||
{ title: "Puppet", mime: "text/x-puppet", highlightJs: "puppet" },
|
||||
{ title: "Python", mime: "text/x-python", highlightJs: "python", default: true },
|
||||
{ title: "Q", mime: "text/x-q", highlightJs: "q" },
|
||||
{ title: "R", mime: "text/x-rsrc", highlightJs: "r" },
|
||||
{ title: "reStructuredText", mime: "text/x-rst" },
|
||||
{ title: "RPM Changes", mime: "text/x-rpm-changes" },
|
||||
{ title: "RPM Spec", mime: "text/x-rpm-spec" },
|
||||
{ title: "Ruby", mime: "text/x-ruby", highlightJs: "ruby", default: true },
|
||||
{ title: "Rust", mime: "text/x-rustsrc", highlightJs: "rust" },
|
||||
{ title: "SAS", mime: "text/x-sas", highlightJs: "sas" },
|
||||
{ title: "Sass", mime: "text/x-sass" },
|
||||
{ title: "Scala", mime: "text/x-scala" },
|
||||
{ title: "Scheme", mime: "text/x-scheme" },
|
||||
{ title: "SCSS", mime: "text/x-scss", highlightJs: "scss" },
|
||||
{ title: "Shell (bash)", mime: "text/x-sh", highlightJs: "bash", default: true },
|
||||
{ title: "Sieve", mime: "application/sieve" },
|
||||
{ title: "Slim", mime: "text/x-slim" },
|
||||
{ title: "Smalltalk", mime: "text/x-stsrc", highlightJs: "smalltalk" },
|
||||
{ title: "Smarty", mime: "text/x-smarty" },
|
||||
{ title: "SML", mime: "text/x-sml", highlightJs: "sml" },
|
||||
{ title: "Solr", mime: "text/x-solr" },
|
||||
{ title: "Soy", mime: "text/x-soy" },
|
||||
{ title: "SPARQL", mime: "application/sparql-query" },
|
||||
{ title: "Spreadsheet", mime: "text/x-spreadsheet" },
|
||||
{ title: "SQL", mime: "text/x-sql", highlightJs: "sql", default: true },
|
||||
{ title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium", highlightJs: "sql", default: true },
|
||||
{ title: "SQLite", mime: "text/x-sqlite", highlightJs: "sql" },
|
||||
{ title: "Squirrel", mime: "text/x-squirrel" },
|
||||
{ title: "sTeX", mime: "text/x-stex" },
|
||||
{ title: "Stylus", mime: "text/x-styl", highlightJs: "stylus" },
|
||||
{ title: "Swift", mime: "text/x-swift", default: true },
|
||||
{ title: "SystemVerilog", mime: "text/x-systemverilog" },
|
||||
{ title: "Tcl", mime: "text/x-tcl", highlightJs: "tcl" },
|
||||
{ title: "Terraform (HCL)", mime: "text/x-hcl", highlightJs: "terraform", highlightJsSource: "libraries", codeMirrorSource: "libraries/codemirror/hcl.js" },
|
||||
{ title: "Textile", mime: "text/x-textile" },
|
||||
{ title: "TiddlyWiki ", mime: "text/x-tiddlywiki" },
|
||||
{ title: "Tiki wiki", mime: "text/tiki" },
|
||||
{ title: "TOML", mime: "text/x-toml", highlightJs: "ini" },
|
||||
{ title: "Tornado", mime: "text/x-tornado" },
|
||||
{ title: "troff", mime: "text/troff" },
|
||||
{ title: "TTCN_CFG", mime: "text/x-ttcn-cfg" },
|
||||
{ title: "TTCN", mime: "text/x-ttcn" },
|
||||
{ title: "Turtle", mime: "text/turtle" },
|
||||
{ title: "Twig", mime: "text/x-twig", highlightJs: "twig" },
|
||||
{ title: "TypeScript-JSX", mime: "text/typescript-jsx" },
|
||||
{ title: "TypeScript", mime: "application/typescript", highlightJs: "typescript" },
|
||||
{ title: "VB.NET", mime: "text/x-vb", highlightJs: "vbnet" },
|
||||
{ title: "VBScript", mime: "text/vbscript", highlightJs: "vbscript" },
|
||||
{ title: "Velocity", mime: "text/velocity" },
|
||||
{ title: "Verilog", mime: "text/x-verilog", highlightJs: "verilog" },
|
||||
{ title: "VHDL", mime: "text/x-vhdl", highlightJs: "vhdl" },
|
||||
{ title: "Vue.js Component", mime: "text/x-vue" },
|
||||
{ title: "Web IDL", mime: "text/x-webidl" },
|
||||
{ title: "XML", mime: "text/xml", highlightJs: "xml", default: true },
|
||||
{ title: "XQuery", mime: "application/xquery", highlightJs: "xquery" },
|
||||
{ title: "xu", mime: "text/x-xu" },
|
||||
{ title: "Yacas", mime: "text/x-yacas" },
|
||||
{ title: "YAML", mime: "text/x-yaml", highlightJs: "yaml", default: true },
|
||||
{ title: "Z80", mime: "text/x-z80" }
|
||||
]);
|
||||
|
||||
/**
|
||||
* Given a MIME type in the usual format (e.g. `text/csrc`), it returns a MIME type that can be passed down to the CKEditor
|
||||
* code plugin.
|
||||
*
|
||||
* @param mimeType The MIME type to normalize, in the usual format (e.g. `text/c-src`).
|
||||
* @returns the normalized MIME type (e.g. `text-c-src`).
|
||||
*/
|
||||
export function normalizeMimeTypeForCKEditor(mimeType: string) {
|
||||
return mimeType.toLowerCase().replace(/[\W_]+/g, "-");
|
||||
}
|
||||
|
||||
let byHighlightJsNameMappings: Record<string, MimeTypeDefinition> | null = null;
|
||||
|
||||
/**
|
||||
* Given a Highlight.js language tag (e.g. `css`), it returns a corresponding {@link MimeTypeDefinition} if found.
|
||||
*
|
||||
* If there are multiple {@link MimeTypeDefinition}s for the language tag, then only the first one is retrieved. For example for `javascript`, the "JS frontend" mime type is returned.
|
||||
*
|
||||
* @param highlightJsName a language tag.
|
||||
* @returns the corresponding {@link MimeTypeDefinition} if found, or `undefined` otherwise.
|
||||
*/
|
||||
export function getMimeTypeFromHighlightJs(highlightJsName: string) {
|
||||
if (!byHighlightJsNameMappings) {
|
||||
byHighlightJsNameMappings = {};
|
||||
for (const mimeType of MIME_TYPES_DICT) {
|
||||
if (mimeType.highlightJs && !byHighlightJsNameMappings[mimeType.highlightJs]) {
|
||||
byHighlightJsNameMappings[mimeType.highlightJs] = mimeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return byHighlightJsNameMappings[highlightJsName];
|
||||
}
|
||||
@@ -69,7 +69,7 @@ function info(message: string | Error) {
|
||||
log(message);
|
||||
}
|
||||
|
||||
function error(message: string | Error) {
|
||||
function error(message: string | Error | unknown) {
|
||||
log(`ERROR: ${message}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import log from "../services/log.js";
|
||||
import type SNote from "./shaca/entities/snote.js";
|
||||
import type SBranch from "./shaca/entities/sbranch.js";
|
||||
import type SAttachment from "./shaca/entities/sattachment.js";
|
||||
import utils, { safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||
import utils, { isDev, safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||
import options from "../services/options.js";
|
||||
|
||||
function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } {
|
||||
@@ -159,7 +159,16 @@ function register(router: Router) {
|
||||
const { header, content, isEmpty } = contentRenderer.getContent(note);
|
||||
const subRoot = getSharedSubTreeRoot(note);
|
||||
const showLoginInShareTheme = options.getOption("showLoginInShareTheme");
|
||||
const opts = { note, header, content, isEmpty, subRoot, assetPath, appPath, showLoginInShareTheme };
|
||||
const opts = {
|
||||
note,
|
||||
header,
|
||||
content,
|
||||
isEmpty,
|
||||
subRoot,
|
||||
assetPath: isDev ? assetPath : `../${assetPath}`,
|
||||
appPath: isDev ? appPath : `../${appPath}`,
|
||||
showLoginInShareTheme
|
||||
};
|
||||
let useDefaultView = true;
|
||||
|
||||
// Check if the user has their own template
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import sessionParser from "./routes/session_parser.js";
|
||||
import fs from "fs";
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
@@ -79,6 +77,7 @@ async function startTrilium() {
|
||||
|
||||
const httpServer = startHttpServer(app);
|
||||
|
||||
const sessionParser = (await import("./routes/session_parser.js")).default;
|
||||
ws.init(httpServer, sessionParser as any); // TODO: Not sure why session parser is incompatible.
|
||||
|
||||
if (utils.isElectron) {
|
||||
|
||||
@@ -13,17 +13,6 @@ function buildFilesToCopy() {
|
||||
});
|
||||
|
||||
const nodePaths = [
|
||||
"@excalidraw/excalidraw/dist/prod/fonts/",
|
||||
"katex/dist",
|
||||
"boxicons/css",
|
||||
"boxicons/fonts",
|
||||
"jquery/dist",
|
||||
"jquery-hotkeys",
|
||||
"autocomplete.js/dist",
|
||||
"normalize.css/normalize.css",
|
||||
"jquery.fancytree/dist",
|
||||
"@highlightjs/cdn-assets",
|
||||
|
||||
// Required as they are native dependencies and cannot be well bundled.
|
||||
"better-sqlite3",
|
||||
"bindings",
|
||||
|
||||
Reference in New Issue
Block a user