Merge branch 'develop' into feat/add-rootless-dockerfiles

This commit is contained in:
perf3ct
2025-05-21 12:58:05 -07:00
156 changed files with 4906 additions and 6149 deletions

View File

@@ -1,2 +1,3 @@
TRILIUM_ENV=production
TRILIUM_DATA_DIR=./apps/server/data
TRILIUM_DATA_DIR=./apps/server/data
TRILIUM_PORT=8082

View File

@@ -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 \

View File

@@ -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

View File

@@ -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",

View File

@@ -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");
});

View File

@@ -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")));

View File

@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
data TEXT,
expires INTEGER
);

View File

@@ -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
);

View File

@@ -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. Its
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" -&gt; "Options" -&gt; "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" -&gt; "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" -&gt; "Options" -&gt; "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&nbsp;<a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a>&nbsp;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" -&gt; "Options" -&gt; "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>

View File

@@ -40,7 +40,7 @@
<h2>Color schemes</h2>
<p>Since Trilium 0.94.0 the colors of code notes can be customized by going&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_4TIF1oA4VQRO">Options</a>&nbsp;→ Code Notes and looking for the <em>Appearance</em> section.</p>
class="reference-link" href="#root/_help_4TIF1oA4VQRO">Options</a>&nbsp;→ 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>

View File

@@ -193,11 +193,6 @@
"special_notes": {
"search_prefix": "搜索:"
},
"code_block": {
"theme_none": "无语法高亮",
"theme_group_light": "浅色主题",
"theme_group_dark": "深色主题"
},
"test_sync": {
"not-configured": "同步服务器主机未配置。请先配置同步。",
"successful": "同步服务器握手成功,同步已开始。"

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -185,11 +185,6 @@
"special_notes": {
"search_prefix": "搜尋:"
},
"code_block": {
"theme_none": "無格式高亮",
"theme_group_light": "淺色主題",
"theme_group_dark": "深色主題"
},
"test_sync": {
"not-configured": "並未設定同步伺服器主機,請先設定同步",
"successful": "成功與同步伺服器握手,現在開始同步"

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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" />

View File

@@ -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">

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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 {

View File

@@ -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/);
});

View 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();
}
});
};

View File

@@ -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
};

View File

@@ -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;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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;
}

View File

@@ -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)"
}

View File

@@ -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)
);

View File

@@ -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[] {

View File

@@ -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
};

View File

@@ -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];
}

View File

@@ -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}`);
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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",