From 2c6ba9ba2cbc8b7eff96560f25919f8ffa64010e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 13 Jun 2025 17:42:11 +0300 Subject: [PATCH 001/250] refactor(share): extract note rendering logic --- apps/server/src/share/content_renderer.ts | 91 +++++++++++++++++- apps/server/src/share/routes.ts | 108 +++------------------- 2 files changed, 102 insertions(+), 97 deletions(-) diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index c1c16e48d..e94eb4297 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -1,10 +1,18 @@ import { JSDOM } from "jsdom"; import shaca from "./shaca/shaca.js"; -import assetPath from "../services/asset_path.js"; +import assetPath, { assetUrlFragment } from "../services/asset_path.js"; import shareRoot from "./share_root.js"; import escapeHtml from "escape-html"; import type SNote from "./shaca/entities/snote.js"; import { t } from "i18next"; +import SBranch from "./shaca/entities/sbranch.js"; +import options from "../services/options.js"; +import { getResourceDir, isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; +import app_path from "../services/app_path.js"; +import ejs from "ejs"; +import log from "../services/log.js"; +import { join } from "path"; +import { readFileSync } from "fs"; /** * Represents the output of the content renderer. @@ -16,6 +24,87 @@ export interface Result { isEmpty?: boolean; } +function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } { + if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) { + // share root itself is not shared + return {}; + } + + // every path leads to share root, but which one to choose? + // for the sake of simplicity, URLs are not note paths + const parentBranch = note.getParentBranches()[0]; + + if (parentBranch.parentNoteId === shareRoot.SHARE_ROOT_NOTE_ID) { + return { + note, + branch: parentBranch + }; + } + + return getSharedSubTreeRoot(parentBranch.getParentNote()); +} + +export function renderNoteContent(note: SNote) { + const { header, content, isEmpty } = getContent(note); + const subRoot = getSharedSubTreeRoot(note); + const showLoginInShareTheme = options.getOption("showLoginInShareTheme"); + const opts = { + note, + header, + content, + isEmpty, + subRoot, + assetPath: isDev ? assetPath : `../${assetPath}`, + assetUrlFragment, + appPath: isDev ? app_path : `../${app_path}`, + showLoginInShareTheme, + t, + isDev + }; + + // Check if the user has their own template. + if (note.hasRelation("shareTemplate")) { + // Get the template note and content + const templateId = note.getRelation("shareTemplate")?.value; + const templateNote = templateId && shaca.getNote(templateId); + + // Make sure the note type is correct + if (templateNote && templateNote.type === "code" && templateNote.mime === "application/x-ejs") { + // EJS caches the result of this so we don't need to pre-cache + const includer = (path: string) => { + const childNote = templateNote.children.find((n) => path === n.title); + if (!childNote) throw new Error(`Unable to find child note: ${path}.`); + if (childNote.type !== "code" || childNote.mime !== "application/x-ejs") throw new Error("Incorrect child note type."); + + const template = childNote.getContent(); + if (typeof template !== "string") throw new Error("Invalid template content type."); + + return { template }; + }; + + // Try to render user's template, w/ fallback to default view + try { + const content = templateNote.getContent(); + if (typeof content === "string") { + return ejs.render(content, opts, { includer }); + } + } catch (e: unknown) { + const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); + log.error(`Rendering user provided share template (${templateId}) threw exception ${errMessage} with stacktrace: ${errStack}`); + } + } + } + + // Render with the default view otherwise. + const templatePath = join(getResourceDir(), "share-theme", "templates", "page.ejs"); + return ejs.render(readFileSync(templatePath, "utf-8"), opts, { + includer: (path) => { + const templatePath = join(getResourceDir(), "share-theme", "templates", `${path}.ejs`); + return { filename: templatePath } + } + }); +} + function getContent(note: SNote) { if (note.isProtected) { return { diff --git a/apps/server/src/share/routes.ts b/apps/server/src/share/routes.ts index 7e18ca505..4b0281ec5 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -4,40 +4,12 @@ import type { Request, Response, Router } from "express"; import shaca from "./shaca/shaca.js"; import shacaLoader from "./shaca/shaca_loader.js"; -import shareRoot from "./share_root.js"; -import contentRenderer from "./content_renderer.js"; -import assetPath, { assetUrlFragment } from "../services/asset_path.js"; -import appPath from "../services/app_path.js"; import searchService from "../services/search/services/search.js"; import SearchContext from "../services/search/search_context.js"; -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, { isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; -import options from "../services/options.js"; -import { t } from "i18next"; -import ejs from "ejs"; - -function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } { - if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) { - // share root itself is not shared - return {}; - } - - // every path leads to share root, but which one to choose? - // for the sake of simplicity, URLs are not note paths - const parentBranch = note.getParentBranches()[0]; - - if (parentBranch.parentNoteId === shareRoot.SHARE_ROOT_NOTE_ID) { - return { - note, - branch: parentBranch - }; - } - - return getSharedSubTreeRoot(parentBranch.getParentNote()); -} +import utils from "../services/utils.js"; +import { renderNoteContent } from "./content_renderer.js"; function addNoIndexHeader(note: SNote, res: Response) { if (note.isLabelTruthy("shareDisallowRobotIndexing")) { @@ -108,8 +80,7 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri let svgString = ""; const attachment = image.getAttachmentByTitle(attachmentName); if (!attachment) { - res.status(404); - renderDefault(res, "404"); + return; } const content = attachment.getContent(); @@ -137,12 +108,19 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri res.send(svg); } +function render404(res: Response) { + res.status(404); + const shareThemePath = `../../share-theme/templates/404.ejs`; + res.render(shareThemePath); +} + function register(router: Router) { + function renderNote(note: SNote, req: Request, res: Response) { if (!note) { console.log("Unable to find note ", note); res.status(404); - renderDefault(res, "404"); + render404(res); return; } @@ -159,63 +137,7 @@ function register(router: Router) { return; } - - const { header, content, isEmpty } = contentRenderer.getContent(note); - const subRoot = getSharedSubTreeRoot(note); - const showLoginInShareTheme = options.getOption("showLoginInShareTheme"); - const opts = { - note, - header, - content, - isEmpty, - subRoot, - assetPath: isDev ? assetPath : `../${assetPath}`, - assetUrlFragment, - appPath: isDev ? appPath : `../${appPath}`, - showLoginInShareTheme, - t, - isDev - }; - let useDefaultView = true; - - // Check if the user has their own template - if (note.hasRelation("shareTemplate")) { - // Get the template note and content - const templateId = note.getRelation("shareTemplate")?.value; - const templateNote = templateId && shaca.getNote(templateId); - - // Make sure the note type is correct - if (templateNote && templateNote.type === "code" && templateNote.mime === "application/x-ejs") { - // EJS caches the result of this so we don't need to pre-cache - const includer = (path: string) => { - const childNote = templateNote.children.find((n) => path === n.title); - if (!childNote) throw new Error(`Unable to find child note: ${path}.`); - if (childNote.type !== "code" || childNote.mime !== "application/x-ejs") throw new Error("Incorrect child note type."); - - const template = childNote.getContent(); - if (typeof template !== "string") throw new Error("Invalid template content type."); - - return { template }; - }; - - // Try to render user's template, w/ fallback to default view - try { - const content = templateNote.getContent(); - if (typeof content === "string") { - const ejsResult = ejs.render(content, opts, { includer }); - res.send(ejsResult); - useDefaultView = false; // Rendering went okay, don't use default view - } - } catch (e: unknown) { - const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); - log.error(`Rendering user provided share template (${templateId}) threw exception ${errMessage} with stacktrace: ${errStack}`); - } - } - } - - if (useDefaultView) { - renderDefault(res, "page", opts); - } + res.send(renderNoteContent(note)); } router.get("/share/", (req, res) => { @@ -399,12 +321,6 @@ function register(router: Router) { }); } -function renderDefault(res: Response>, template: "page" | "404", opts: any = {}) { - // Path is relative to apps/server/dist/assets/views - const shareThemePath = `../../share-theme/templates/${template}.ejs`; - res.render(shareThemePath, opts); -} - export default { register }; From 9c460dbc87791a3ecbe36d54b3784f261ef40926 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 13 Jun 2025 23:10:11 +0300 Subject: [PATCH 002/250] feat(export/zip): get same rendering engine as share --- apps/server/src/becca/entities/bbranch.ts | 5 +++++ apps/server/src/becca/entities/bnote.ts | 16 +++++++++++++++ apps/server/src/services/export/zip.ts | 16 ++++++++++----- apps/server/src/share/content_renderer.ts | 25 +++++++++++++++-------- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/apps/server/src/becca/entities/bbranch.ts b/apps/server/src/becca/entities/bbranch.ts index 00e3ec4b7..b31cadd71 100644 --- a/apps/server/src/becca/entities/bbranch.ts +++ b/apps/server/src/becca/entities/bbranch.ts @@ -278,6 +278,11 @@ class BBranch extends AbstractBeccaEntity { }); } } + + getParentNote() { + return this.parentNote; + } + } export default BBranch; diff --git a/apps/server/src/becca/entities/bnote.ts b/apps/server/src/becca/entities/bnote.ts index 419c9bdfe..650316777 100644 --- a/apps/server/src/becca/entities/bnote.ts +++ b/apps/server/src/becca/entities/bnote.ts @@ -1758,6 +1758,22 @@ class BNote extends AbstractBeccaEntity { return childBranches; } + get encodedTitle() { + return encodeURIComponent(this.title); + } + + getVisibleChildBranches() { + return this.getChildBranches().filter((branch) => !branch.getNote().isLabelTruthy("shareHiddenFromTree")); + } + + getVisibleChildNotes() { + return this.getVisibleChildBranches().map((branch) => branch.getNote()); + } + + hasVisibleChildren() { + return this.getVisibleChildNotes().length > 0; + } + } export default BNote; diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 81c67a21b..f544f2a73 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -19,9 +19,11 @@ import type NoteMeta from "../meta/note_meta.js"; import type AttachmentMeta from "../meta/attachment_meta.js"; import type AttributeMeta from "../meta/attribute_meta.js"; import type BBranch from "../../becca/entities/bbranch.js"; +import type BNote from "../../becca/entities/bnote.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; import cssContent from "@triliumnext/ckeditor5/content.css"; +import { renderNoteContent } from "../../share/content_renderer.js"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -314,7 +316,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h } } - function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { + function prepareContent(note: BNote | undefined, title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { if (["html", "markdown"].includes(noteMeta?.format || "")) { content = content.toString(); content = rewriteFn(content, noteMeta); @@ -329,8 +331,11 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`; const htmlTitle = escapeHtml(title); - // element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 - content = ` + if (note) { + content = renderNoteContent(note); + } else { + // element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 + content = ` @@ -346,6 +351,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h `; + } } return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content; @@ -375,7 +381,7 @@ ${markdownContent}`; let content: string | Buffer = `

This is a clone of a note. Go to its primary location.

`; - content = prepareContent(noteMeta.title, content, noteMeta); + content = prepareContent(undefined, noteMeta.title, content, noteMeta); archive.append(content, { name: filePathPrefix + noteMeta.dataFileName }); @@ -391,7 +397,7 @@ ${markdownContent}`; } if (noteMeta.dataFileName) { - const content = prepareContent(noteMeta.title, note.getContent(), noteMeta); + const content = prepareContent(note, noteMeta.title, note.getContent(), noteMeta); archive.append(content, { name: filePathPrefix + noteMeta.dataFileName, diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index e94eb4297..aad3ab9d2 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -4,6 +4,8 @@ import assetPath, { assetUrlFragment } from "../services/asset_path.js"; import shareRoot from "./share_root.js"; import escapeHtml from "escape-html"; import type SNote from "./shaca/entities/snote.js"; +import BNote from "../becca/entities/bnote.js"; +import type BBranch from "../becca/entities/bbranch.js"; import { t } from "i18next"; import SBranch from "./shaca/entities/sbranch.js"; import options from "../services/options.js"; @@ -24,8 +26,8 @@ export interface Result { isEmpty?: boolean; } -function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } { - if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) { +function getSharedSubTreeRoot(note: SNote | BNote | undefined): { note?: SNote | BNote; branch?: SBranch | BBranch } { + if (!note || note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) { // share root itself is not shared return {}; } @@ -34,6 +36,13 @@ function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } { // for the sake of simplicity, URLs are not note paths const parentBranch = note.getParentBranches()[0]; + if (note instanceof BNote) { + return { + note, + branch: parentBranch + } + } + if (parentBranch.parentNoteId === shareRoot.SHARE_ROOT_NOTE_ID) { return { note, @@ -44,7 +53,7 @@ function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } { return getSharedSubTreeRoot(parentBranch.getParentNote()); } -export function renderNoteContent(note: SNote) { +export function renderNoteContent(note: SNote | BNote) { const { header, content, isEmpty } = getContent(note); const subRoot = getSharedSubTreeRoot(note); const showLoginInShareTheme = options.getOption("showLoginInShareTheme"); @@ -105,7 +114,7 @@ export function renderNoteContent(note: SNote) { }); } -function getContent(note: SNote) { +function getContent(note: SNote | BNote) { if (note.isProtected) { return { header: "", @@ -154,7 +163,7 @@ function renderIndex(result: Result) { result.content += ""; } -function renderText(result: Result, note: SNote) { +function renderText(result: Result, note: SNote | BNote) { const document = new JSDOM(result.content || "").window.document; result.isEmpty = document.body.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0; @@ -247,7 +256,7 @@ export function renderCode(result: Result) { } } -function renderMermaid(result: Result, note: SNote) { +function renderMermaid(result: Result, note: SNote | BNote) { if (typeof result.content !== "string") { return; } @@ -261,11 +270,11 @@ function renderMermaid(result: Result, note: SNote) { `; } -function renderImage(result: Result, note: SNote) { +function renderImage(result: Result, note: SNote | BNote) { result.content = ``; } -function renderFile(note: SNote, result: Result) { +function renderFile(note: SNote | BNote, result: Result) { if (note.mime === "application/pdf") { result.content = ``; } else { From f189deb415e4ee2b67d9a9480c56634ede1c5d20 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 13 Jun 2025 23:22:44 +0300 Subject: [PATCH 003/250] feat(export/zip): get tree to render --- apps/server/src/services/export/zip.ts | 4 ++-- apps/server/src/share/content_renderer.ts | 26 +++++++++++++++++---- packages/share-theme/src/templates/page.ejs | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index f544f2a73..a175241d8 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -23,7 +23,7 @@ import type BNote from "../../becca/entities/bnote.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; import cssContent from "@triliumnext/ckeditor5/content.css"; -import { renderNoteContent } from "../../share/content_renderer.js"; +import { renderNoteForExport } from "../../share/content_renderer.js"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -332,7 +332,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h const htmlTitle = escapeHtml(title); if (note) { - content = renderNoteContent(note); + content = renderNoteForExport(note, branch); } else { // element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 content = ` diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index aad3ab9d2..465c605ca 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -26,7 +26,12 @@ export interface Result { isEmpty?: boolean; } -function getSharedSubTreeRoot(note: SNote | BNote | undefined): { note?: SNote | BNote; branch?: SBranch | BBranch } { +interface Subroot { + note?: SNote | BNote; + branch?: SBranch | BBranch +} + +function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot { if (!note || note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) { // share root itself is not shared return {}; @@ -53,9 +58,21 @@ function getSharedSubTreeRoot(note: SNote | BNote | undefined): { note?: SNote | return getSharedSubTreeRoot(parentBranch.getParentNote()); } -export function renderNoteContent(note: SNote | BNote) { - const { header, content, isEmpty } = getContent(note); +export function renderNoteForExport(note: BNote, parentBranch: BBranch) { + const subRoot: Subroot = { + branch: parentBranch, + note: parentBranch.getNote() + }; + return renderNoteContentInternal(note, subRoot, note.getParentNotes()[0].noteId); +} + +export function renderNoteContent(note: SNote) { const subRoot = getSharedSubTreeRoot(note); + return renderNoteContentInternal(note, subRoot, "_share"); +} + +function renderNoteContentInternal(note: SNote | BNote, subRoot: Subroot, rootNoteId: string) { + const { header, content, isEmpty } = getContent(note); const showLoginInShareTheme = options.getOption("showLoginInShareTheme"); const opts = { note, @@ -68,7 +85,8 @@ export function renderNoteContent(note: SNote | BNote) { appPath: isDev ? app_path : `../${app_path}`, showLoginInShareTheme, t, - isDev + isDev, + rootNoteId }; // Check if the user has their own template. diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index 5c39051eb..ba3d45b44 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -108,7 +108,7 @@ content = content.replaceAll(headingRe, (...match) => { <% const ancestors = []; let notePointer = note; - while (notePointer.parents[0].noteId !== "_share") { + while (notePointer.parents[0].noteId !== rootNoteId) { const pointerParent = notePointer.parents[0]; ancestors.push(pointerParent.noteId); notePointer = pointerParent; From 4d5e866db6f1d526a4f1b5e1947e4cc227fed1d8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 13 Jun 2025 23:47:04 +0300 Subject: [PATCH 004/250] feat(export/zip): get CSS to load --- apps/server/src/services/export/zip.ts | 25 +++++++++--- apps/server/src/share/content_renderer.ts | 45 +++++++++++++++++---- packages/share-theme/src/templates/page.ejs | 10 +---- 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index a175241d8..8d8d2de07 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -2,11 +2,11 @@ import html from "html"; import dateUtils from "../date_utils.js"; -import path from "path"; +import path, { join } from "path"; import mimeTypes from "mime-types"; import mdService from "./markdown.js"; import packageInfo from "../../../package.json" with { type: "json" }; -import { getContentDisposition, escapeHtml, getResourceDir } from "../utils.js"; +import { getContentDisposition, escapeHtml, getResourceDir, isDev } from "../utils.js"; import protectedSessionService from "../protected_session.js"; import sanitize from "sanitize-filename"; import fs from "fs"; @@ -22,7 +22,7 @@ import type BBranch from "../../becca/entities/bbranch.js"; import type BNote from "../../becca/entities/bnote.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; -import cssContent from "@triliumnext/ckeditor5/content.css"; +//import cssContent from "@triliumnext/ckeditor5/content.css"; import { renderNoteForExport } from "../../share/content_renderer.js"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -328,12 +328,13 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h throw new Error("Missing note path."); } - const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`; + const basePath = "../".repeat(noteMeta.notePath.length - 1); const htmlTitle = escapeHtml(title); if (note) { - content = renderNoteForExport(note, branch); + content = renderNoteForExport(note, branch, basePath); } else { + const cssUrl = basePath + "style.css"; // element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 content = ` @@ -518,6 +519,7 @@ ${markdownContent}`; return; } + let cssContent = getShareThemeAssets("css"); archive.append(cssContent, { name: cssMeta.dataFileName }); } @@ -629,6 +631,19 @@ async function exportToZipFile(noteId: string, format: "markdown" | "html", zipF log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`); } +function getShareThemeAssets(extension: string) { + let path: string | undefined; + if (isDev) { + path = join(getResourceDir(), "..", "..", "client", "dist", "src", `share.${extension}`); + } + + if (!path) { + throw new Error("Not yet defined."); + } + + return fs.readFileSync(path, "utf-8"); +} + export default { exportToZip, exportToZipFile diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 465c605ca..4fbae0586 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -16,6 +16,8 @@ import log from "../services/log.js"; import { join } from "path"; import { readFileSync } from "fs"; +const shareAdjustedAssetPath = isDev ? assetPath : `../${assetPath}`; + /** * Represents the output of the content renderer. */ @@ -58,20 +60,50 @@ function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot { return getSharedSubTreeRoot(parentBranch.getParentNote()); } -export function renderNoteForExport(note: BNote, parentBranch: BBranch) { +export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath: string) { const subRoot: Subroot = { branch: parentBranch, note: parentBranch.getNote() }; - return renderNoteContentInternal(note, subRoot, note.getParentNotes()[0].noteId); + + return renderNoteContentInternal(note, { + subRoot, + rootNoteId: note.getParentNotes()[0].noteId, + cssToLoad: [ + `${basePath}style.css` + ] + }); } export function renderNoteContent(note: SNote) { const subRoot = getSharedSubTreeRoot(note); - return renderNoteContentInternal(note, subRoot, "_share"); + + // Determine CSS to load. + const cssToLoad: string[] = []; + if (!isDev && !note.isLabelTruthy("shareOmitDefaultCss")) { + cssToLoad.push(`${shareAdjustedAssetPath}/src/share.css`); + cssToLoad.push(`${shareAdjustedAssetPath}/src/boxicons.css`); + } + + // Support custom CSS too. + for (const cssRelation of note.getRelations("shareCss")) { + cssToLoad.push(`api/notes/${cssRelation.value}/download`); + } + + return renderNoteContentInternal(note, { + subRoot, + rootNoteId: "_share", + cssToLoad + }); } -function renderNoteContentInternal(note: SNote | BNote, subRoot: Subroot, rootNoteId: string) { +interface RenderArgs { + subRoot: Subroot; + rootNoteId: string; + cssToLoad: string[]; +} + +function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) { const { header, content, isEmpty } = getContent(note); const showLoginInShareTheme = options.getOption("showLoginInShareTheme"); const opts = { @@ -79,14 +111,13 @@ function renderNoteContentInternal(note: SNote | BNote, subRoot: Subroot, rootNo header, content, isEmpty, - subRoot, - assetPath: isDev ? assetPath : `../${assetPath}`, + assetPath: shareAdjustedAssetPath, assetUrlFragment, appPath: isDev ? app_path : `../${app_path}`, showLoginInShareTheme, t, isDev, - rootNoteId + ...renderArgs }; // Check if the user has their own template. diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index ba3d45b44..820e32ad5 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -6,14 +6,8 @@ api/notes/<%= note.getRelation("shareFavicon").value %>/download<% } else { %>../favicon.ico<% } %>"> - - <% if (!isDev && !note.isLabelTruthy("shareOmitDefaultCss")) { %> - - - <% } %> - - <% for (const cssRelation of note.getRelations("shareCss")) { %> - + <% for (const url of cssToLoad) { %> + <% } %> <% for (const jsRelation of note.getRelations("shareJs")) { %> From d8958adea5c13ced699db18fff9dace3db7689dd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 14 Jun 2025 00:07:55 +0300 Subject: [PATCH 005/250] feat(export/zip): basic tree navigation --- apps/server/src/services/export/zip.ts | 3 +++ packages/share-theme/src/templates/tree_item.ejs | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 8d8d2de07..bb3e382be 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -333,6 +333,9 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h if (note) { content = renderNoteForExport(note, branch, basePath); + + // TODO: Fix double rewrite. + content = rewriteFn(content, noteMeta); } else { const cssUrl = basePath + "style.css"; // element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 diff --git a/packages/share-theme/src/templates/tree_item.ejs b/packages/share-theme/src/templates/tree_item.ejs index b033ad2bc..99da978fa 100644 --- a/packages/share-theme/src/templates/tree_item.ejs +++ b/packages/share-theme/src/templates/tree_item.ejs @@ -1,7 +1,16 @@ <% const linkClass = `type-${note.type}` + (activeNote.noteId === note.noteId ? " active" : ""); const isExternalLink = note.hasLabel("shareExternal"); -const linkHref = isExternalLink ? note.getLabelValue("shareExternal") : `./${note.shareId}`; +let linkHref; + +if (isExternalLink) { + linkHref = note.getLabelValue("shareExternal"); +} else if (note.shareId) { + linkHref = `./${note.shareId}`; +} else { + linkHref = `#${note.getBestNotePath().join("/")}`; +} + const target = isExternalLink ? ` target="_blank" rel="noopener noreferrer"` : ""; %> From 01a552ceb515ff62fd7ec17f9367ed52fac3c2ed Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 14 Jun 2025 00:52:56 +0300 Subject: [PATCH 006/250] feat(export/zip): get boxicons to work --- apps/server/src/services/export/zip.ts | 53 +++++++++++++++-------- apps/server/src/share/content_renderer.ts | 3 +- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index bb3e382be..62a1be37e 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -517,13 +517,15 @@ ${markdownContent}`; archive.append(fullHtml, { name: indexMeta.dataFileName }); } - function saveCss(rootMeta: NoteMeta, cssMeta: NoteMeta) { - if (!cssMeta.dataFileName) { - return; - } + function saveAssets(rootMeta: NoteMeta, assetsMeta: NoteMeta[]) { + for (const assetMeta of assetsMeta) { + if (!assetMeta.dataFileName) { + continue; + } - let cssContent = getShareThemeAssets("css"); - archive.append(cssContent, { name: cssMeta.dataFileName }); + let cssContent = getShareThemeAssets(assetMeta.dataFileName); + archive.append(cssContent, { name: assetMeta.dataFileName }); + } } const existingFileNames: Record = format === "html" ? { navigation: 0, index: 1 } : {}; @@ -540,7 +542,7 @@ ${markdownContent}`; let navigationMeta: NoteMeta | null = null; let indexMeta: NoteMeta | null = null; - let cssMeta: NoteMeta | null = null; + let assetsMeta: NoteMeta[] = []; if (format === "html") { navigationMeta = { @@ -557,12 +559,24 @@ ${markdownContent}`; metaFile.files.push(indexMeta); - cssMeta = { - noImport: true, - dataFileName: "style.css" - }; + const assets = [ + "style.css", + "boxicons.css", + "boxicons.eot", + "boxicons.woff2", + "boxicons.woff", + "boxicons.ttf", + "boxicons.svg", + ]; - metaFile.files.push(cssMeta); + for (const asset of assets) { + const assetMeta = { + noImport: true, + dataFileName: asset + }; + assetsMeta.push(assetMeta); + metaFile.files.push(assetMeta); + } } for (const noteMeta of Object.values(noteIdToMeta)) { @@ -596,13 +610,13 @@ ${markdownContent}`; saveNote(rootMeta, ""); if (format === "html") { - if (!navigationMeta || !indexMeta || !cssMeta) { + if (!navigationMeta || !indexMeta || !assetsMeta) { throw new Error("Missing meta."); } saveNavigation(rootMeta, navigationMeta); saveIndex(rootMeta, indexMeta); - saveCss(rootMeta, cssMeta); + saveAssets(rootMeta, assetsMeta); } const note = branch.getNote(); @@ -634,17 +648,22 @@ async function exportToZipFile(noteId: string, format: "markdown" | "html", zipF log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`); } -function getShareThemeAssets(extension: string) { +function getShareThemeAssets(nameWithExtension: string) { + // Rename share.css to style.css. + if (nameWithExtension === "style.css") { + nameWithExtension = "share.css"; + } + let path: string | undefined; if (isDev) { - path = join(getResourceDir(), "..", "..", "client", "dist", "src", `share.${extension}`); + path = join(getResourceDir(), "..", "..", "client", "dist", "src", nameWithExtension); } if (!path) { throw new Error("Not yet defined."); } - return fs.readFileSync(path, "utf-8"); + return fs.readFileSync(path); } export default { diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 4fbae0586..0a85f17d2 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -70,7 +70,8 @@ export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath subRoot, rootNoteId: note.getParentNotes()[0].noteId, cssToLoad: [ - `${basePath}style.css` + `${basePath}style.css`, + `${basePath}boxicons.css` ] }); } From d3115e834ad808071d3c52ac4f7bce0f2d4520bb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 14 Jun 2025 01:01:12 +0300 Subject: [PATCH 007/250] feat(export/zip): get logo to work --- apps/server/src/services/export/zip.ts | 6 +++++- apps/server/src/share/content_renderer.ts | 10 ++++++++-- packages/share-theme/src/templates/page.ejs | 2 -- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 62a1be37e..046b7369c 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -24,6 +24,7 @@ import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; //import cssContent from "@triliumnext/ckeditor5/content.css"; import { renderNoteForExport } from "../../share/content_renderer.js"; +import { RESOURCE_DIR } from "../resource_dir.js"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -567,6 +568,7 @@ ${markdownContent}`; "boxicons.woff", "boxicons.ttf", "boxicons.svg", + "icon-color.svg" ]; for (const asset of assets) { @@ -655,7 +657,9 @@ function getShareThemeAssets(nameWithExtension: string) { } let path: string | undefined; - if (isDev) { + if (nameWithExtension === "icon-color.svg") { + path = join(RESOURCE_DIR, "images", nameWithExtension); + } else if (isDev) { path = join(getResourceDir(), "..", "..", "client", "dist", "src", nameWithExtension); } diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 0a85f17d2..346d9743e 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -72,7 +72,8 @@ export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath cssToLoad: [ `${basePath}style.css`, `${basePath}boxicons.css` - ] + ], + logoUrl: `${basePath}icon-color.svg` }); } @@ -91,10 +92,14 @@ export function renderNoteContent(note: SNote) { cssToLoad.push(`api/notes/${cssRelation.value}/download`); } + const customLogoId = note.getRelation("shareLogo")?.value; + const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`; + return renderNoteContentInternal(note, { subRoot, rootNoteId: "_share", - cssToLoad + cssToLoad, + logoUrl }); } @@ -102,6 +107,7 @@ interface RenderArgs { subRoot: Subroot; rootNoteId: string; cssToLoad: string[]; + logoUrl: string; } function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) { diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index 820e32ad5..8b4609020 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -49,8 +49,6 @@ <% -const customLogoId = subRoot.note.getRelation("shareLogo")?.value; -const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`; const logoWidth = subRoot.note.getLabelValue("shareLogoWidth") ?? 53; const logoHeight = subRoot.note.getLabelValue("shareLogoHeight") ?? 40; const mobileLogoHeight = logoHeight && logoWidth ? 32 / (logoWidth / logoHeight) : ""; From 01beebf660c8f11c0b0fe576b72a535524e0b107 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 14 Jun 2025 01:23:02 +0300 Subject: [PATCH 008/250] feat(export/zip): load script as well --- apps/server/src/services/export/zip.ts | 3 +++ apps/server/src/share/content_renderer.ts | 18 +++++++++++++++--- packages/share-theme/src/templates/page.ejs | 4 ++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 046b7369c..7034f8e18 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -562,6 +562,7 @@ ${markdownContent}`; const assets = [ "style.css", + "script.js", "boxicons.css", "boxicons.eot", "boxicons.woff2", @@ -654,6 +655,8 @@ function getShareThemeAssets(nameWithExtension: string) { // Rename share.css to style.css. if (nameWithExtension === "style.css") { nameWithExtension = "share.css"; + } else if (nameWithExtension === "script.js") { + nameWithExtension = "share.js"; } let path: string | undefined; diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 346d9743e..041e5cc17 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -17,6 +17,7 @@ import { join } from "path"; import { readFileSync } from "fs"; const shareAdjustedAssetPath = isDev ? assetPath : `../${assetPath}`; +const shareAdjustedAppPath = isDev ? app_path : `../${app_path}`; /** * Represents the output of the content renderer. @@ -73,6 +74,9 @@ export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath `${basePath}style.css`, `${basePath}boxicons.css` ], + jsToLoad: [ + `${basePath}script.js` + ], logoUrl: `${basePath}icon-color.svg` }); } @@ -86,12 +90,18 @@ export function renderNoteContent(note: SNote) { cssToLoad.push(`${shareAdjustedAssetPath}/src/share.css`); cssToLoad.push(`${shareAdjustedAssetPath}/src/boxicons.css`); } - - // Support custom CSS too. for (const cssRelation of note.getRelations("shareCss")) { cssToLoad.push(`api/notes/${cssRelation.value}/download`); } + // Determine JS to load. + const jsToLoad: string[] = [ + `${shareAdjustedAppPath}/share.js` + ]; + for (const jsRelation of note.getRelations("shareJs")) { + jsToLoad.push(`api/notes/${jsRelation.value}/download`); + } + const customLogoId = note.getRelation("shareLogo")?.value; const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`; @@ -99,6 +109,7 @@ export function renderNoteContent(note: SNote) { subRoot, rootNoteId: "_share", cssToLoad, + jsToLoad, logoUrl }); } @@ -107,6 +118,7 @@ interface RenderArgs { subRoot: Subroot; rootNoteId: string; cssToLoad: string[]; + jsToLoad: string[]; logoUrl: string; } @@ -120,7 +132,7 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) isEmpty, assetPath: shareAdjustedAssetPath, assetUrlFragment, - appPath: isDev ? app_path : `../${app_path}`, + appPath: shareAdjustedAppPath, showLoginInShareTheme, t, isDev, diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index 8b4609020..243f788a1 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -9,8 +9,8 @@ <% for (const url of cssToLoad) { %> <% } %> - <% for (const jsRelation of note.getRelations("shareJs")) { %> - + <% for (const url of jsToLoad) { %> + <% } %> <% if (note.hasLabel("shareDisallowRobotIndexing")) { %> From c5196721d4ffe3ad0c05d99c5f85b5ee0ac022dd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 15:36:10 +0300 Subject: [PATCH 009/250] chore(nx): sync tsconfig --- apps/server/tsconfig.app.json | 3 --- apps/server/tsconfig.json | 3 --- 2 files changed, 6 deletions(-) diff --git a/apps/server/tsconfig.app.json b/apps/server/tsconfig.app.json index eb7f102aa..61f4a77fe 100644 --- a/apps/server/tsconfig.app.json +++ b/apps/server/tsconfig.app.json @@ -34,9 +34,6 @@ "src/**/*.spec.jsx" ], "references": [ - { - "path": "../../packages/ckeditor5/tsconfig.lib.json" - }, { "path": "../../packages/turndown-plugin-gfm/tsconfig.lib.json" }, diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 6bc224295..baacd3fa5 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -3,9 +3,6 @@ "files": [], "include": [], "references": [ - { - "path": "../../packages/ckeditor5" - }, { "path": "../../packages/turndown-plugin-gfm" }, From dfd575b6ebb0be9d2c0b2aae962478eed634dcbf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 16:08:31 +0300 Subject: [PATCH 010/250] refactor(export/zip): extract into separate provider --- apps/server/src/services/export/zip.ts | 207 +++--------------- .../services/export/zip/abstract_provider.ts | 27 +++ apps/server/src/services/export/zip/html.ts | 135 ++++++++++++ 3 files changed, 188 insertions(+), 181 deletions(-) create mode 100644 apps/server/src/services/export/zip/abstract_provider.ts create mode 100644 apps/server/src/services/export/zip/html.ts diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 7034f8e18..6caffac86 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -2,11 +2,11 @@ import html from "html"; import dateUtils from "../date_utils.js"; -import path, { join } from "path"; +import path from "path"; import mimeTypes from "mime-types"; import mdService from "./markdown.js"; import packageInfo from "../../../package.json" with { type: "json" }; -import { getContentDisposition, escapeHtml, getResourceDir, isDev } from "../utils.js"; +import { getContentDisposition, escapeHtml } from "../utils.js"; import protectedSessionService from "../protected_session.js"; import sanitize from "sanitize-filename"; import fs from "fs"; @@ -19,12 +19,10 @@ import type NoteMeta from "../meta/note_meta.js"; import type AttachmentMeta from "../meta/attachment_meta.js"; import type AttributeMeta from "../meta/attribute_meta.js"; import type BBranch from "../../becca/entities/bbranch.js"; -import type BNote from "../../becca/entities/bnote.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; -//import cssContent from "@triliumnext/ckeditor5/content.css"; -import { renderNoteForExport } from "../../share/content_renderer.js"; -import { RESOURCE_DIR } from "../resource_dir.js"; +import HtmlExportProvider from "./zip/html.js"; +import { ZipExportProvider } from "./zip/abstract_provider.js"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -317,7 +315,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h } } - function prepareContent(note: BNote | undefined, title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { + function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { if (["html", "markdown"].includes(noteMeta?.format || "")) { content = content.toString(); content = rewriteFn(content, noteMeta); @@ -329,18 +327,11 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h throw new Error("Missing note path."); } - const basePath = "../".repeat(noteMeta.notePath.length - 1); + const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`; const htmlTitle = escapeHtml(title); - if (note) { - content = renderNoteForExport(note, branch, basePath); - - // TODO: Fix double rewrite. - content = rewriteFn(content, noteMeta); - } else { - const cssUrl = basePath + "style.css"; - // element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 - content = ` + // element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 + content = ` @@ -356,7 +347,6 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h `; - } } return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content; @@ -386,7 +376,7 @@ ${markdownContent}`; let content: string | Buffer = `

This is a clone of a note. Go to its primary location.

`; - content = prepareContent(undefined, noteMeta.title, content, noteMeta); + content = prepareContent(noteMeta.title, content, noteMeta); archive.append(content, { name: filePathPrefix + noteMeta.dataFileName }); @@ -402,7 +392,7 @@ ${markdownContent}`; } if (noteMeta.dataFileName) { - const content = prepareContent(note, noteMeta.title, note.getContent(), noteMeta); + const content = prepareContent(noteMeta.title, note.getContent(), noteMeta); archive.append(content, { name: filePathPrefix + noteMeta.dataFileName, @@ -438,97 +428,6 @@ ${markdownContent}`; } } - function saveNavigation(rootMeta: NoteMeta, navigationMeta: NoteMeta) { - if (!navigationMeta.dataFileName) { - return; - } - - function saveNavigationInner(meta: NoteMeta) { - let html = "
  • "; - - const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`); - - if (meta.dataFileName && meta.noteId) { - const targetUrl = getNoteTargetUrl(meta.noteId, rootMeta); - - html += `${escapedTitle}`; - } else { - html += escapedTitle; - } - - if (meta.children && meta.children.length > 0) { - html += "
      "; - - for (const child of meta.children) { - html += saveNavigationInner(child); - } - - html += "
    "; - } - - return `${html}
  • `; - } - - const fullHtml = ` - - - - - -
      ${saveNavigationInner(rootMeta)}
    - -`; - const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml; - - archive.append(prettyHtml, { name: navigationMeta.dataFileName }); - } - - function saveIndex(rootMeta: NoteMeta, indexMeta: NoteMeta) { - let firstNonEmptyNote; - let curMeta = rootMeta; - - if (!indexMeta.dataFileName) { - return; - } - - while (!firstNonEmptyNote) { - if (curMeta.dataFileName && curMeta.noteId) { - firstNonEmptyNote = getNoteTargetUrl(curMeta.noteId, rootMeta); - } - - if (curMeta.children && curMeta.children.length > 0) { - curMeta = curMeta.children[0]; - } else { - break; - } - } - - const fullHtml = ` - - - - - - - - - -`; - - archive.append(fullHtml, { name: indexMeta.dataFileName }); - } - - function saveAssets(rootMeta: NoteMeta, assetsMeta: NoteMeta[]) { - for (const assetMeta of assetsMeta) { - if (!assetMeta.dataFileName) { - continue; - } - - let cssContent = getShareThemeAssets(assetMeta.dataFileName); - archive.append(cssContent, { name: assetMeta.dataFileName }); - } - } - const existingFileNames: Record = format === "html" ? { navigation: 0, index: 1 } : {}; const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames); if (!rootMeta) { @@ -541,47 +440,23 @@ ${markdownContent}`; files: [rootMeta] }; - let navigationMeta: NoteMeta | null = null; - let indexMeta: NoteMeta | null = null; - let assetsMeta: NoteMeta[] = []; - - if (format === "html") { - navigationMeta = { - noImport: true, - dataFileName: "navigation.html" - }; - - metaFile.files.push(navigationMeta); - - indexMeta = { - noImport: true, - dataFileName: "index.html" - }; - - metaFile.files.push(indexMeta); - - const assets = [ - "style.css", - "script.js", - "boxicons.css", - "boxicons.eot", - "boxicons.woff2", - "boxicons.woff", - "boxicons.ttf", - "boxicons.svg", - "icon-color.svg" - ]; - - for (const asset of assets) { - const assetMeta = { - noImport: true, - dataFileName: asset - }; - assetsMeta.push(assetMeta); - metaFile.files.push(assetMeta); - } + let provider: ZipExportProvider; + switch (format) { + case "html": + provider = new HtmlExportProvider({ + getNoteTargetUrl, + metaFile, + archive, + rootMeta + }); + break; + case "markdown": + default: + throw new Error(); } + provider.prepareMeta(); + for (const noteMeta of Object.values(noteIdToMeta)) { // filter out relations which are not inside this export noteMeta.attributes = (noteMeta.attributes || []).filter((attr) => { @@ -612,15 +487,7 @@ ${markdownContent}`; saveNote(rootMeta, ""); - if (format === "html") { - if (!navigationMeta || !indexMeta || !assetsMeta) { - throw new Error("Missing meta."); - } - - saveNavigation(rootMeta, navigationMeta); - saveIndex(rootMeta, indexMeta); - saveAssets(rootMeta, assetsMeta); - } + provider.afterDone(); const note = branch.getNote(); const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`; @@ -651,28 +518,6 @@ async function exportToZipFile(noteId: string, format: "markdown" | "html", zipF log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`); } -function getShareThemeAssets(nameWithExtension: string) { - // Rename share.css to style.css. - if (nameWithExtension === "style.css") { - nameWithExtension = "share.css"; - } else if (nameWithExtension === "script.js") { - nameWithExtension = "share.js"; - } - - let path: string | undefined; - if (nameWithExtension === "icon-color.svg") { - path = join(RESOURCE_DIR, "images", nameWithExtension); - } else if (isDev) { - path = join(getResourceDir(), "..", "..", "client", "dist", "src", nameWithExtension); - } - - if (!path) { - throw new Error("Not yet defined."); - } - - return fs.readFileSync(path); -} - export default { exportToZip, exportToZipFile diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts new file mode 100644 index 000000000..264dde0a7 --- /dev/null +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -0,0 +1,27 @@ +import { Archiver } from "archiver"; +import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js"; + +interface ZipExportProviderData { + getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; + metaFile: NoteMetaFile; + rootMeta: NoteMeta; + archive: Archiver; +} + +export abstract class ZipExportProvider { + + metaFile: NoteMetaFile; + getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; + rootMeta: NoteMeta; + archive: Archiver; + + constructor(data: ZipExportProviderData) { + this.metaFile = data.metaFile; + this.getNoteTargetUrl = data.getNoteTargetUrl; + this.rootMeta = data.rootMeta; + this.archive = data.archive; + } + + abstract prepareMeta(): void; + abstract afterDone(): void; +} diff --git a/apps/server/src/services/export/zip/html.ts b/apps/server/src/services/export/zip/html.ts new file mode 100644 index 000000000..517552e1d --- /dev/null +++ b/apps/server/src/services/export/zip/html.ts @@ -0,0 +1,135 @@ +import type NoteMeta from "../../meta/note_meta.js"; +import { escapeHtml } from "../../utils"; +import cssContent from "@triliumnext/ckeditor5/content.css"; +import html from "html"; +import { ZipExportProvider } from "./abstract_provider.js"; + +export default class HtmlExportProvider extends ZipExportProvider { + + private navigationMeta: NoteMeta | null = null; + private indexMeta: NoteMeta | null = null; + private cssMeta: NoteMeta | null = null; + + prepareMeta() { + this.navigationMeta = { + noImport: true, + dataFileName: "navigation.html" + }; + + this.metaFile.files.push(this.navigationMeta); + + this.indexMeta = { + noImport: true, + dataFileName: "index.html" + }; + + this.metaFile.files.push(this.indexMeta); + + this.cssMeta = { + noImport: true, + dataFileName: "style.css" + }; + + this.metaFile.files.push(this.cssMeta); + } + + afterDone() { + if (!this.navigationMeta || !this.indexMeta || !this.cssMeta) { + throw new Error("Missing meta."); + } + + this.#saveNavigation(this.rootMeta, this.navigationMeta); + this.#saveIndex(this.rootMeta, this.indexMeta); + this.#saveCss(this.rootMeta, this.cssMeta); + } + + #saveNavigationInner(meta: NoteMeta) { + let html = "
  • "; + + const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`); + + if (meta.dataFileName && meta.noteId) { + const targetUrl = this.getNoteTargetUrl(meta.noteId, this.rootMeta); + + html += `${escapedTitle}`; + } else { + html += escapedTitle; + } + + if (meta.children && meta.children.length > 0) { + html += "
      "; + + for (const child of meta.children) { + html += this.#saveNavigationInner(child); + } + + html += "
    "; + } + + return `${html}
  • `; + } + + #saveNavigation(rootMeta: NoteMeta, navigationMeta: NoteMeta) { + if (!navigationMeta.dataFileName) { + return; + } + + const fullHtml = ` + + + + + +
      ${this.#saveNavigationInner(rootMeta)}
    + + `; + const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml; + + this.archive.append(prettyHtml, { name: navigationMeta.dataFileName }); + } + + #saveIndex(rootMeta: NoteMeta, indexMeta: NoteMeta) { + let firstNonEmptyNote; + let curMeta = rootMeta; + + if (!indexMeta.dataFileName) { + return; + } + + while (!firstNonEmptyNote) { + if (curMeta.dataFileName && curMeta.noteId) { + firstNonEmptyNote = this.getNoteTargetUrl(curMeta.noteId, rootMeta); + } + + if (curMeta.children && curMeta.children.length > 0) { + curMeta = curMeta.children[0]; + } else { + break; + } + } + + const fullHtml = ` + + + + + + + + + +`; + + this.archive.append(fullHtml, { name: indexMeta.dataFileName }); + } + + #saveCss(rootMeta: NoteMeta, cssMeta: NoteMeta) { + if (!cssMeta.dataFileName) { + return; + } + + this.archive.append(cssContent, { name: cssMeta.dataFileName }); + } + +} + From e529633b8bde4a36b83c982d24c1c397dcfb6bf1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 16:17:29 +0300 Subject: [PATCH 011/250] chore(export/zip): bring back markdown exporter --- apps/server/src/services/export/zip.ts | 18 +++++++++++------- .../services/export/zip/abstract_provider.ts | 2 +- .../server/src/services/export/zip/markdown.ts | 8 ++++++++ 3 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/services/export/zip/markdown.ts diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 6caffac86..f6c131d87 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -22,7 +22,8 @@ import type BBranch from "../../becca/entities/bbranch.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; import HtmlExportProvider from "./zip/html.js"; -import { ZipExportProvider } from "./zip/abstract_provider.js"; +import { ZipExportProvider, ZipExportProviderData } from "./zip/abstract_provider.js"; +import MarkdownExportProvider from "./zip/markdown.js"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -441,16 +442,19 @@ ${markdownContent}`; }; let provider: ZipExportProvider; + const providerData: ZipExportProviderData = { + getNoteTargetUrl, + metaFile, + archive, + rootMeta + }; switch (format) { case "html": - provider = new HtmlExportProvider({ - getNoteTargetUrl, - metaFile, - archive, - rootMeta - }); + provider = new HtmlExportProvider(providerData); break; case "markdown": + provider = new MarkdownExportProvider(providerData); + break; default: throw new Error(); } diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts index 264dde0a7..5f3502107 100644 --- a/apps/server/src/services/export/zip/abstract_provider.ts +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -1,7 +1,7 @@ import { Archiver } from "archiver"; import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js"; -interface ZipExportProviderData { +export interface ZipExportProviderData { getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; metaFile: NoteMetaFile; rootMeta: NoteMeta; diff --git a/apps/server/src/services/export/zip/markdown.ts b/apps/server/src/services/export/zip/markdown.ts new file mode 100644 index 000000000..2f8ac13bc --- /dev/null +++ b/apps/server/src/services/export/zip/markdown.ts @@ -0,0 +1,8 @@ +import { ZipExportProvider } from "./abstract_provider" + +export default class MarkdownExportProvider extends ZipExportProvider { + + prepareMeta() { } + afterDone() { } + +} From 55bb2fdb9b855e68a1df6006e9858b8eb5287052 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 16:22:42 +0300 Subject: [PATCH 012/250] refactor(export/zip): extract prepare content into providers --- apps/server/src/services/export/zip.ts | 64 +------------------ .../services/export/zip/abstract_provider.ts | 22 +++++++ apps/server/src/services/export/zip/html.ts | 35 ++++++++++ .../src/services/export/zip/markdown.ts | 18 ++++++ 4 files changed, 77 insertions(+), 62 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index f6c131d87..84d84871c 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -1,10 +1,8 @@ "use strict"; -import html from "html"; import dateUtils from "../date_utils.js"; import path from "path"; import mimeTypes from "mime-types"; -import mdService from "./markdown.js"; import packageInfo from "../../../package.json" with { type: "json" }; import { getContentDisposition, escapeHtml } from "../utils.js"; import protectedSessionService from "../protected_session.js"; @@ -22,27 +20,9 @@ import type BBranch from "../../becca/entities/bbranch.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; import HtmlExportProvider from "./zip/html.js"; -import { ZipExportProvider, ZipExportProviderData } from "./zip/abstract_provider.js"; +import { AdvancedExportOptions, ZipExportProvider, ZipExportProviderData } from "./zip/abstract_provider.js"; import MarkdownExportProvider from "./zip/markdown.js"; -type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; - -export interface AdvancedExportOptions { - /** - * If `true`, then only the note's content will be kept. If `false` (default), then each page will have its own template. - */ - skipHtmlTemplate?: boolean; - - /** - * Provides a custom function to rewrite the links found in HTML or Markdown notes. This method is called for every note imported, if it's of the right type. - * - * @param originalRewriteLinks the original rewrite links function. Can be used to access the default behaviour without having to reimplement it. - * @param getNoteTargetUrl the method to obtain a note's target URL, used internally by `originalRewriteLinks` but can be used here as well. - * @returns a function to rewrite the links in HTML or Markdown notes. - */ - customRewriteLinks?: (originalRewriteLinks: RewriteLinksFn, getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null) => RewriteLinksFn; -} - async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) { if (!["html", "markdown"].includes(format)) { throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`); @@ -322,47 +302,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h content = rewriteFn(content, noteMeta); } - if (noteMeta.format === "html" && typeof content === "string") { - if (!content.substr(0, 100).toLowerCase().includes(" element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 - content = ` - - - - - - ${htmlTitle} - - -
    -

    ${htmlTitle}

    - -
    ${content}
    -
    - -`; - } - - return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content; - } else if (noteMeta.format === "markdown" && typeof content === "string") { - let markdownContent = mdService.toMarkdown(content); - - if (markdownContent.trim().length > 0 && !markdownContent.startsWith("# ")) { - markdownContent = `# ${title}\r -${markdownContent}`; - } - - return markdownContent; - } else { - return content; - } + return provider.prepareContent(title, content, noteMeta); } function saveNote(noteMeta: NoteMeta, filePathPrefix: string) { diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts index 5f3502107..ceadc0ec1 100644 --- a/apps/server/src/services/export/zip/abstract_provider.ts +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -1,11 +1,30 @@ import { Archiver } from "archiver"; import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js"; +type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; + +export interface AdvancedExportOptions { + /** + * If `true`, then only the note's content will be kept. If `false` (default), then each page will have its own template. + */ + skipHtmlTemplate?: boolean; + + /** + * Provides a custom function to rewrite the links found in HTML or Markdown notes. This method is called for every note imported, if it's of the right type. + * + * @param originalRewriteLinks the original rewrite links function. Can be used to access the default behaviour without having to reimplement it. + * @param getNoteTargetUrl the method to obtain a note's target URL, used internally by `originalRewriteLinks` but can be used here as well. + * @returns a function to rewrite the links in HTML or Markdown notes. + */ + customRewriteLinks?: (originalRewriteLinks: RewriteLinksFn, getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null) => RewriteLinksFn; +} + export interface ZipExportProviderData { getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; metaFile: NoteMetaFile; rootMeta: NoteMeta; archive: Archiver; + zipExportOptions?: AdvancedExportOptions; } export abstract class ZipExportProvider { @@ -14,14 +33,17 @@ export abstract class ZipExportProvider { getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; rootMeta: NoteMeta; archive: Archiver; + zipExportOptions?: AdvancedExportOptions; constructor(data: ZipExportProviderData) { this.metaFile = data.metaFile; this.getNoteTargetUrl = data.getNoteTargetUrl; this.rootMeta = data.rootMeta; this.archive = data.archive; + this.zipExportOptions = data.zipExportOptions; } abstract prepareMeta(): void; + abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer; abstract afterDone(): void; } diff --git a/apps/server/src/services/export/zip/html.ts b/apps/server/src/services/export/zip/html.ts index 517552e1d..0eac07fb8 100644 --- a/apps/server/src/services/export/zip/html.ts +++ b/apps/server/src/services/export/zip/html.ts @@ -33,6 +33,41 @@ export default class HtmlExportProvider extends ZipExportProvider { this.metaFile.files.push(this.cssMeta); } + prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { + if (noteMeta.format === "html" && typeof content === "string") { + if (!content.substr(0, 100).toLowerCase().includes(" element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 + content = ` + + + + + + ${htmlTitle} + + +
    +

    ${htmlTitle}

    + +
    ${content}
    +
    + +`; + } + + return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content; + } else { + return content; + } + } + afterDone() { if (!this.navigationMeta || !this.indexMeta || !this.cssMeta) { throw new Error("Missing meta."); diff --git a/apps/server/src/services/export/zip/markdown.ts b/apps/server/src/services/export/zip/markdown.ts index 2f8ac13bc..9143e5e1f 100644 --- a/apps/server/src/services/export/zip/markdown.ts +++ b/apps/server/src/services/export/zip/markdown.ts @@ -1,8 +1,26 @@ +import NoteMeta from "../../meta/note_meta" import { ZipExportProvider } from "./abstract_provider" +import mdService from "../markdown.js"; export default class MarkdownExportProvider extends ZipExportProvider { prepareMeta() { } + + prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { + if (noteMeta.format === "markdown" && typeof content === "string") { + let markdownContent = mdService.toMarkdown(content); + + if (markdownContent.trim().length > 0 && !markdownContent.startsWith("# ")) { + markdownContent = `# ${title}\r +${markdownContent}`; + } + + return markdownContent; + } else { + return content; + } + } + afterDone() { } } From a9f68f548778e828b2f7d0e1a72198b8ae939330 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 18:13:47 +0300 Subject: [PATCH 013/250] feat(export/zip): add option to export with share theme --- apps/client/src/widgets/dialogs/export.ts | 7 ++ apps/server/src/etapi/notes.ts | 2 +- apps/server/src/routes/api/export.ts | 2 +- apps/server/src/services/export/zip.ts | 32 ++++--- .../services/export/zip/abstract_provider.ts | 4 +- .../src/services/export/zip/share_theme.ts | 86 +++++++++++++++++++ 6 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 apps/server/src/services/export/zip/share_theme.ts diff --git a/apps/client/src/widgets/dialogs/export.ts b/apps/client/src/widgets/dialogs/export.ts index d9b13f4ed..ccc748d01 100644 --- a/apps/client/src/widgets/dialogs/export.ts +++ b/apps/client/src/widgets/dialogs/export.ts @@ -85,6 +85,13 @@ const TPL = /*html*/` + +
    + +
    diff --git a/apps/server/src/etapi/notes.ts b/apps/server/src/etapi/notes.ts index 973ec04af..82280d0b9 100644 --- a/apps/server/src/etapi/notes.ts +++ b/apps/server/src/etapi/notes.ts @@ -147,7 +147,7 @@ function register(router: Router) { const note = eu.getAndCheckNote(req.params.noteId); const format = req.query.format || "html"; - if (typeof format !== "string" || !["html", "markdown"].includes(format)) { + if (typeof format !== "string" || !["html", "markdown", "share"].includes(format)) { throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`); } diff --git a/apps/server/src/routes/api/export.ts b/apps/server/src/routes/api/export.ts index 7433cd552..b2909f288 100644 --- a/apps/server/src/routes/api/export.ts +++ b/apps/server/src/routes/api/export.ts @@ -26,7 +26,7 @@ function exportBranch(req: Request, res: Response) { const taskContext = new TaskContext(taskId, "export"); try { - if (type === "subtree" && (format === "html" || format === "markdown")) { + if (type === "subtree" && (format === "html" || format === "markdown" || format === "share")) { zipExportService.exportToZip(taskContext, branch, format, res); } else if (type === "single") { if (format !== "html" && format !== "markdown") { diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 84d84871c..de84a580d 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -4,7 +4,7 @@ import dateUtils from "../date_utils.js"; import path from "path"; import mimeTypes from "mime-types"; import packageInfo from "../../../package.json" with { type: "json" }; -import { getContentDisposition, escapeHtml } from "../utils.js"; +import { getContentDisposition } from "../utils.js"; import protectedSessionService from "../protected_session.js"; import sanitize from "sanitize-filename"; import fs from "fs"; @@ -22,9 +22,11 @@ import type { NoteMetaFile } from "../meta/note_meta.js"; import HtmlExportProvider from "./zip/html.js"; import { AdvancedExportOptions, ZipExportProvider, ZipExportProviderData } from "./zip/abstract_provider.js"; import MarkdownExportProvider from "./zip/markdown.js"; +import ShareThemeExportProvider from "./zip/share_theme.js"; +import type BNote from "../../becca/entities/bnote.js"; -async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) { - if (!["html", "markdown"].includes(format)) { +async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown" | "share", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) { + if (!["html", "markdown", "share"].includes(format)) { throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`); } @@ -135,7 +137,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h prefix: branch.prefix, dataFileName: fileName, type: "text", // export will have text description - format: format + format: (format === "markdown" ? "markdown" : "html") }; return meta; } @@ -165,7 +167,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h taskContext.increaseProgressCount(); if (note.type === "text") { - meta.format = format; + meta.format = (format === "markdown" ? "markdown" : "html"); } noteIdToMeta[note.noteId] = meta as NoteMeta; @@ -296,13 +298,18 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h } } - function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { - if (["html", "markdown"].includes(noteMeta?.format || "")) { + function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note?: BNote): string | Buffer { + const isText = ["html", "markdown"].includes(noteMeta?.format || ""); + if (isText) { content = content.toString(); - content = rewriteFn(content, noteMeta); } - return provider.prepareContent(title, content, noteMeta); + content = provider.prepareContent(title, content, noteMeta, note, branch); + if (isText) { + content = rewriteFn(content as string, noteMeta); + } + + return content; } function saveNote(noteMeta: NoteMeta, filePathPrefix: string) { @@ -317,7 +324,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h let content: string | Buffer = `

    This is a clone of a note. Go to its primary location.

    `; - content = prepareContent(noteMeta.title, content, noteMeta); + content = prepareContent(noteMeta.title, content, noteMeta, undefined); archive.append(content, { name: filePathPrefix + noteMeta.dataFileName }); @@ -333,7 +340,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h } if (noteMeta.dataFileName) { - const content = prepareContent(noteMeta.title, note.getContent(), noteMeta); + const content = prepareContent(noteMeta.title, note.getContent(), noteMeta, note); archive.append(content, { name: filePathPrefix + noteMeta.dataFileName, @@ -395,6 +402,9 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h case "markdown": provider = new MarkdownExportProvider(providerData); break; + case "share": + provider = new ShareThemeExportProvider(providerData); + break; default: throw new Error(); } diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts index ceadc0ec1..ba57ba69f 100644 --- a/apps/server/src/services/export/zip/abstract_provider.ts +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -1,5 +1,7 @@ import { Archiver } from "archiver"; import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js"; +import type BNote from "../../../becca/entities/bnote.js"; +import type BBranch from "../../../becca/entities/bbranch.js"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -44,6 +46,6 @@ export abstract class ZipExportProvider { } abstract prepareMeta(): void; - abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer; + abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer; abstract afterDone(): void; } diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts new file mode 100644 index 000000000..50339d20a --- /dev/null +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -0,0 +1,86 @@ +import { join } from "path"; +import NoteMeta from "../../meta/note_meta"; +import { ZipExportProvider } from "./abstract_provider"; +import { RESOURCE_DIR } from "../../resource_dir"; +import { getResourceDir, isDev } from "../../utils"; +import fs from "fs"; +import { renderNoteForExport } from "../../../share/content_renderer"; +import type BNote from "../../../becca/entities/bnote.js"; +import type BBranch from "../../../becca/entities/bbranch.js"; + +export default class ShareThemeExportProvider extends ZipExportProvider { + + private assetsMeta: NoteMeta[] = []; + + prepareMeta(): void { + const assets = [ + "style.css", + "script.js", + "boxicons.css", + "boxicons.eot", + "boxicons.woff2", + "boxicons.woff", + "boxicons.ttf", + "boxicons.svg", + "icon-color.svg" + ]; + + for (const asset of assets) { + const assetMeta = { + noImport: true, + dataFileName: asset + }; + this.assetsMeta.push(assetMeta); + this.metaFile.files.push(assetMeta); + } + } + + prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote, branch: BBranch): string | Buffer { + if (!noteMeta?.notePath?.length) { + throw new Error("Missing note path."); + } + const basePath = "../".repeat(noteMeta.notePath.length - 1); + + content = renderNoteForExport(note, branch, basePath); + + return content; + } + + afterDone(): void { + this.#saveAssets(this.rootMeta, this.assetsMeta); + } + + #saveAssets(rootMeta: NoteMeta, assetsMeta: NoteMeta[]) { + for (const assetMeta of assetsMeta) { + if (!assetMeta.dataFileName) { + continue; + } + + let cssContent = getShareThemeAssets(assetMeta.dataFileName); + this.archive.append(cssContent, { name: assetMeta.dataFileName }); + } + } + +} + +function getShareThemeAssets(nameWithExtension: string) { + // Rename share.css to style.css. + if (nameWithExtension === "style.css") { + nameWithExtension = "share.css"; + } else if (nameWithExtension === "script.js") { + nameWithExtension = "share.js"; + } + + let path: string | undefined; + if (nameWithExtension === "icon-color.svg") { + path = join(RESOURCE_DIR, "images", nameWithExtension); + } else if (isDev) { + path = join(getResourceDir(), "..", "..", "client", "dist", "src", nameWithExtension); + } + + if (!path) { + throw new Error("Not yet defined."); + } + + return fs.readFileSync(path); +} From acb0991d054100350d5b80a242025b830f947e15 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 18:24:59 +0300 Subject: [PATCH 014/250] refactor(export/zip): separate building provider into own method --- apps/server/src/services/export/zip.ts | 42 +++++++++---------- .../src/services/export/zip/share_theme.ts | 6 ++- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index de84a580d..fe767662b 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -20,7 +20,7 @@ import type BBranch from "../../becca/entities/bbranch.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; import HtmlExportProvider from "./zip/html.js"; -import { AdvancedExportOptions, ZipExportProvider, ZipExportProviderData } from "./zip/abstract_provider.js"; +import { AdvancedExportOptions, ZipExportProviderData } from "./zip/abstract_provider.js"; import MarkdownExportProvider from "./zip/markdown.js"; import ShareThemeExportProvider from "./zip/share_theme.js"; import type BNote from "../../becca/entities/bnote.js"; @@ -36,6 +36,25 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h const noteIdToMeta: Record = {}; + function buildProvider() { + const providerData: ZipExportProviderData = { + getNoteTargetUrl, + metaFile, + archive, + rootMeta: rootMeta! + }; + switch (format) { + case "html": + return new HtmlExportProvider(providerData); + case "markdown": + return new MarkdownExportProvider(providerData); + case "share": + return new ShareThemeExportProvider(providerData); + default: + throw new Error(); + } + } + function getUniqueFilename(existingFileNames: Record, fileName: string) { const lcFileName = fileName.toLowerCase(); @@ -388,26 +407,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h files: [rootMeta] }; - let provider: ZipExportProvider; - const providerData: ZipExportProviderData = { - getNoteTargetUrl, - metaFile, - archive, - rootMeta - }; - switch (format) { - case "html": - provider = new HtmlExportProvider(providerData); - break; - case "markdown": - provider = new MarkdownExportProvider(providerData); - break; - case "share": - provider = new ShareThemeExportProvider(providerData); - break; - default: - throw new Error(); - } + const provider= buildProvider(); provider.prepareMeta(); diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 50339d20a..07dbf5f7c 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -35,13 +35,15 @@ export default class ShareThemeExportProvider extends ZipExportProvider { } } - prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote, branch: BBranch): string | Buffer { + prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer { if (!noteMeta?.notePath?.length) { throw new Error("Missing note path."); } const basePath = "../".repeat(noteMeta.notePath.length - 1); - content = renderNoteForExport(note, branch, basePath); + if (note) { + content = renderNoteForExport(note, branch, basePath); + } return content; } From 0efdf65202f7dc48dd9e69a89b4f3846f6d6c887 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 18:46:21 +0300 Subject: [PATCH 015/250] refactor(export/share): build index file --- apps/server/src/services/export/zip.ts | 5 +++-- .../services/export/zip/abstract_provider.ts | 4 +++- .../src/services/export/zip/share_theme.ts | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index fe767662b..9a11ca026 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -16,7 +16,7 @@ import ValidationError from "../../errors/validation_error.js"; import type NoteMeta from "../meta/note_meta.js"; import type AttachmentMeta from "../meta/attachment_meta.js"; import type AttributeMeta from "../meta/attribute_meta.js"; -import type BBranch from "../../becca/entities/bbranch.js"; +import BBranch from "../../becca/entities/bbranch.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; import HtmlExportProvider from "./zip/html.js"; @@ -41,7 +41,8 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h getNoteTargetUrl, metaFile, archive, - rootMeta: rootMeta! + rootMeta: rootMeta!, + branch }; switch (format) { case "html": diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts index ba57ba69f..d1bd7a9f9 100644 --- a/apps/server/src/services/export/zip/abstract_provider.ts +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -22,6 +22,7 @@ export interface AdvancedExportOptions { } export interface ZipExportProviderData { + branch: BBranch; getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; metaFile: NoteMetaFile; rootMeta: NoteMeta; @@ -30,7 +31,7 @@ export interface ZipExportProviderData { } export abstract class ZipExportProvider { - + branch: BBranch; metaFile: NoteMetaFile; getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; rootMeta: NoteMeta; @@ -38,6 +39,7 @@ export abstract class ZipExportProvider { zipExportOptions?: AdvancedExportOptions; constructor(data: ZipExportProviderData) { + this.branch = data.branch; this.metaFile = data.metaFile; this.getNoteTargetUrl = data.getNoteTargetUrl; this.rootMeta = data.rootMeta; diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 07dbf5f7c..04a4a633f 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -11,6 +11,7 @@ import type BBranch from "../../../becca/entities/bbranch.js"; export default class ShareThemeExportProvider extends ZipExportProvider { private assetsMeta: NoteMeta[] = []; + private indexMeta: NoteMeta | null = null; prepareMeta(): void { const assets = [ @@ -33,6 +34,13 @@ export default class ShareThemeExportProvider extends ZipExportProvider { this.assetsMeta.push(assetMeta); this.metaFile.files.push(assetMeta); } + + this.indexMeta = { + noImport: true, + dataFileName: "index.html" + }; + + this.metaFile.files.push(this.indexMeta); } prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer { @@ -50,6 +58,17 @@ export default class ShareThemeExportProvider extends ZipExportProvider { afterDone(): void { this.#saveAssets(this.rootMeta, this.assetsMeta); + this.#saveIndex(); + } + + #saveIndex() { + if (!this.indexMeta?.dataFileName) { + return; + } + + const note = this.branch.getNote(); + const fullHtml = this.prepareContent(this.rootMeta.title ?? "", note.getContent(), this.rootMeta, note, this.branch); + this.archive.append(fullHtml, { name: this.indexMeta.dataFileName }); } #saveAssets(rootMeta: NoteMeta, assetsMeta: NoteMeta[]) { From 8523050ab2ed02ce75eedd30ef1f5efd22c22579 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 19:00:20 +0300 Subject: [PATCH 016/250] fix(export/share): note children preview links not working --- apps/server/src/services/export/zip.ts | 9 +++------ apps/server/src/services/export/zip/abstract_provider.ts | 3 +++ apps/server/src/services/export/zip/html.ts | 6 +++++- apps/server/src/services/export/zip/markdown.ts | 1 + apps/server/src/services/export/zip/share_theme.ts | 1 + packages/share-theme/src/templates/page.ejs | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 9a11ca026..2f550898a 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -35,6 +35,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h }); const noteIdToMeta: Record = {}; + const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks); function buildProvider() { const providerData: ZipExportProviderData = { @@ -42,7 +43,8 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h metaFile, archive, rootMeta: rootMeta!, - branch + branch, + rewriteFn }; switch (format) { case "html": @@ -275,8 +277,6 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h return url; } - const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks); - function rewriteLinks(content: string, noteMeta: NoteMeta): string { content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/[^"]*"/g, (match, targetNoteId) => { const url = getNoteTargetUrl(targetNoteId, noteMeta); @@ -325,9 +325,6 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h } content = provider.prepareContent(title, content, noteMeta, note, branch); - if (isText) { - content = rewriteFn(content as string, noteMeta); - } return content; } diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts index d1bd7a9f9..0c7a53656 100644 --- a/apps/server/src/services/export/zip/abstract_provider.ts +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -28,6 +28,7 @@ export interface ZipExportProviderData { rootMeta: NoteMeta; archive: Archiver; zipExportOptions?: AdvancedExportOptions; + rewriteFn: RewriteLinksFn; } export abstract class ZipExportProvider { @@ -37,6 +38,7 @@ export abstract class ZipExportProvider { rootMeta: NoteMeta; archive: Archiver; zipExportOptions?: AdvancedExportOptions; + rewriteFn: RewriteLinksFn; constructor(data: ZipExportProviderData) { this.branch = data.branch; @@ -45,6 +47,7 @@ export abstract class ZipExportProvider { this.rootMeta = data.rootMeta; this.archive = data.archive; this.zipExportOptions = data.zipExportOptions; + this.rewriteFn = data.rewriteFn; } abstract prepareMeta(): void; diff --git a/apps/server/src/services/export/zip/html.ts b/apps/server/src/services/export/zip/html.ts index 0eac07fb8..749d7adc8 100644 --- a/apps/server/src/services/export/zip/html.ts +++ b/apps/server/src/services/export/zip/html.ts @@ -62,7 +62,11 @@ export default class HtmlExportProvider extends ZipExportProvider { `; } - return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content; + if (content.length < 100_000) { + content = html.prettyPrint(content, { indent_size: 2 }) + } + content = this.rewriteFn(content as string, noteMeta); + return content; } else { return content; } diff --git a/apps/server/src/services/export/zip/markdown.ts b/apps/server/src/services/export/zip/markdown.ts index 9143e5e1f..1ace2051a 100644 --- a/apps/server/src/services/export/zip/markdown.ts +++ b/apps/server/src/services/export/zip/markdown.ts @@ -15,6 +15,7 @@ export default class MarkdownExportProvider extends ZipExportProvider { ${markdownContent}`; } + markdownContent = this.rewriteFn(markdownContent, noteMeta); return markdownContent; } else { return content; diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 04a4a633f..695d90e8d 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -51,6 +51,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { if (note) { content = renderNoteForExport(note, branch, basePath); + content = this.rewriteFn(content, noteMeta); } return content; diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index 243f788a1..6900a1be0 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -137,7 +137,7 @@ content = content.replaceAll(headingRe, (...match) => { const action = note.type === "book" ? "getChildNotes" : "getVisibleChildNotes"; for (const childNote of note[action]()) { const isExternalLink = childNote.hasLabel("shareExternal") || childNote.hasLabel("shareExternalLink"); - const linkHref = isExternalLink ? childNote.getLabelValue("shareExternal") ?? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId}`; + const linkHref = isExternalLink ? childNote.getLabelValue("shareExternal") ?? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId ?? "#root/" + childNote.noteId}`; const target = isExternalLink ? ` target="_blank" rel="noopener noreferrer"` : ""; %>
  • From 77e4c3d0ecb05f39e823932e6259819aca8f5748 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 19:25:28 +0300 Subject: [PATCH 017/250] refactor(export/share): use different URL rewriting mechanism --- apps/server/src/becca/entities/bnote.ts | 4 ++++ apps/server/src/services/export/zip/share_theme.ts | 1 + packages/share-theme/src/templates/page.ejs | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/server/src/becca/entities/bnote.ts b/apps/server/src/becca/entities/bnote.ts index 650316777..e6563f2db 100644 --- a/apps/server/src/becca/entities/bnote.ts +++ b/apps/server/src/becca/entities/bnote.ts @@ -1774,6 +1774,10 @@ class BNote extends AbstractBeccaEntity { return this.getVisibleChildNotes().length > 0; } + get shareId() { + return this.noteId; + } + } export default BNote; diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 695d90e8d..2b4ba72e8 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -51,6 +51,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { if (note) { content = renderNoteForExport(note, branch, basePath); + content = content.replace(/href="[^"]*\.\/([a-zA-Z0-9_\/]{12})[^"]*"/g, "href=\"#root/$1\""); content = this.rewriteFn(content, noteMeta); } diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index 6900a1be0..243f788a1 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -137,7 +137,7 @@ content = content.replaceAll(headingRe, (...match) => { const action = note.type === "book" ? "getChildNotes" : "getVisibleChildNotes"; for (const childNote of note[action]()) { const isExternalLink = childNote.hasLabel("shareExternal") || childNote.hasLabel("shareExternalLink"); - const linkHref = isExternalLink ? childNote.getLabelValue("shareExternal") ?? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId ?? "#root/" + childNote.noteId}`; + const linkHref = isExternalLink ? childNote.getLabelValue("shareExternal") ?? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId}`; const target = isExternalLink ? ` target="_blank" rel="noopener noreferrer"` : ""; %>
  • From 35622a212253f09834ace19571118a28a93f423b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 19:38:47 +0300 Subject: [PATCH 018/250] feat(export/share): always render empty files --- apps/server/src/services/export/zip.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 2f550898a..9c0f099d1 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -198,10 +198,13 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h note.sortChildren(); const childBranches = note.getChildBranches().filter((branch) => branch?.noteId !== "_hidden"); - const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable(); + let shouldIncludeFile = (!note.isProtected || protectedSessionService.isProtectedSessionAvailable()); + if (format !== "share") { + shouldIncludeFile = shouldIncludeFile && (note.getContent().length > 0 || childBranches.length === 0); + } // if it's a leaf, then we'll export it even if it's empty - if (available && (note.getContent().length > 0 || childBranches.length === 0)) { + if (shouldIncludeFile) { meta.dataFileName = getDataFileName(note.type, note.mime, baseFileName, existingFileNames); } From b475037127466737cc2eade8601798d8ef052d47 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 20:00:40 +0300 Subject: [PATCH 019/250] feat(export/share): render non-text note types --- apps/server/src/services/export/zip.ts | 38 +++---------------- .../services/export/zip/abstract_provider.ts | 37 ++++++++++++++---- apps/server/src/services/export/zip/html.ts | 27 ++++++------- .../src/services/export/zip/share_theme.ts | 22 ++++++----- 4 files changed, 60 insertions(+), 64 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 9c0f099d1..26af3424f 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -2,7 +2,6 @@ import dateUtils from "../date_utils.js"; import path from "path"; -import mimeTypes from "mime-types"; import packageInfo from "../../../package.json" with { type: "json" }; import { getContentDisposition } from "../utils.js"; import protectedSessionService from "../protected_session.js"; @@ -33,16 +32,15 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h const archive = archiver("zip", { zlib: { level: 9 } // Sets the compression level. }); + const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks); + const provider= buildProvider(); const noteIdToMeta: Record = {}; - const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks); function buildProvider() { const providerData: ZipExportProviderData = { getNoteTargetUrl, - metaFile, archive, - rootMeta: rootMeta!, branch, rewriteFn }; @@ -94,36 +92,14 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h } let existingExtension = path.extname(fileName).toLowerCase(); - let newExtension; - - // the following two are handled specifically since we always want to have these extensions no matter the automatic detection - // and/or existing detected extensions in the note name - if (type === "text" && format === "markdown") { - newExtension = "md"; - } else if (type === "text" && format === "html") { - newExtension = "html"; - } else if (mime === "application/x-javascript" || mime === "text/javascript") { - newExtension = "js"; - } else if (type === "canvas" || mime === "application/json") { - newExtension = "json"; - } else if (existingExtension.length > 0) { - // if the page already has an extension, then we'll just keep it - newExtension = null; - } else { - if (mime?.toLowerCase()?.trim() === "image/jpg") { - newExtension = "jpg"; - } else if (mime?.toLowerCase()?.trim() === "text/mermaid") { - newExtension = "txt"; - } else { - newExtension = mimeTypes.extension(mime) || "dat"; - } - } + const newExtension = provider.mapExtension(type, mime, existingExtension, format); // if the note is already named with the extension (e.g. "image.jpg"), then it's silly to append the exact same extension again if (newExtension && existingExtension !== `.${newExtension.toLowerCase()}`) { fileName += `.${newExtension}`; } + return getUniqueFilename(existingFileNames, fileName); } @@ -408,9 +384,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h files: [rootMeta] }; - const provider= buildProvider(); - - provider.prepareMeta(); + provider.prepareMeta(metaFile); for (const noteMeta of Object.values(noteIdToMeta)) { // filter out relations which are not inside this export @@ -442,7 +416,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h saveNote(rootMeta, ""); - provider.afterDone(); + provider.afterDone(rootMeta); const note = branch.getNote(); const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`; diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts index 0c7a53656..6ca5fdb9a 100644 --- a/apps/server/src/services/export/zip/abstract_provider.ts +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -2,6 +2,7 @@ import { Archiver } from "archiver"; import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js"; import type BNote from "../../../becca/entities/bnote.js"; import type BBranch from "../../../becca/entities/bbranch.js"; +import mimeTypes from "mime-types"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -24,8 +25,6 @@ export interface AdvancedExportOptions { export interface ZipExportProviderData { branch: BBranch; getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; - metaFile: NoteMetaFile; - rootMeta: NoteMeta; archive: Archiver; zipExportOptions?: AdvancedExportOptions; rewriteFn: RewriteLinksFn; @@ -33,24 +32,46 @@ export interface ZipExportProviderData { export abstract class ZipExportProvider { branch: BBranch; - metaFile: NoteMetaFile; getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null; - rootMeta: NoteMeta; archive: Archiver; zipExportOptions?: AdvancedExportOptions; rewriteFn: RewriteLinksFn; constructor(data: ZipExportProviderData) { this.branch = data.branch; - this.metaFile = data.metaFile; this.getNoteTargetUrl = data.getNoteTargetUrl; - this.rootMeta = data.rootMeta; this.archive = data.archive; this.zipExportOptions = data.zipExportOptions; this.rewriteFn = data.rewriteFn; } - abstract prepareMeta(): void; + abstract prepareMeta(metaFile: NoteMetaFile): void; abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer; - abstract afterDone(): void; + abstract afterDone(rootMeta: NoteMeta): void; + + mapExtension(type: string | null, mime: string, existingExtension: string, format: string) { + // the following two are handled specifically since we always want to have these extensions no matter the automatic detection + // and/or existing detected extensions in the note name + if (type === "text" && format === "markdown") { + return "md"; + } else if (type === "text" && format === "html") { + return "html"; + } else if (mime === "application/x-javascript" || mime === "text/javascript") { + return "js"; + } else if (type === "canvas" || mime === "application/json") { + return "json"; + } else if (existingExtension.length > 0) { + // if the page already has an extension, then we'll just keep it + return null; + } else { + if (mime?.toLowerCase()?.trim() === "image/jpg") { + return "jpg"; + } else if (mime?.toLowerCase()?.trim() === "text/mermaid") { + return "txt"; + } else { + return mimeTypes.extension(mime) || "dat"; + } + } + } + } diff --git a/apps/server/src/services/export/zip/html.ts b/apps/server/src/services/export/zip/html.ts index 749d7adc8..8eb5c5d93 100644 --- a/apps/server/src/services/export/zip/html.ts +++ b/apps/server/src/services/export/zip/html.ts @@ -10,27 +10,24 @@ export default class HtmlExportProvider extends ZipExportProvider { private indexMeta: NoteMeta | null = null; private cssMeta: NoteMeta | null = null; - prepareMeta() { + prepareMeta(metaFile) { this.navigationMeta = { noImport: true, dataFileName: "navigation.html" }; - - this.metaFile.files.push(this.navigationMeta); + metaFile.files.push(this.navigationMeta); this.indexMeta = { noImport: true, dataFileName: "index.html" }; - - this.metaFile.files.push(this.indexMeta); + metaFile.files.push(this.indexMeta); this.cssMeta = { noImport: true, dataFileName: "style.css" }; - - this.metaFile.files.push(this.cssMeta); + metaFile.files.push(this.cssMeta); } prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer { @@ -72,23 +69,23 @@ export default class HtmlExportProvider extends ZipExportProvider { } } - afterDone() { + afterDone(rootMeta: NoteMeta) { if (!this.navigationMeta || !this.indexMeta || !this.cssMeta) { throw new Error("Missing meta."); } - this.#saveNavigation(this.rootMeta, this.navigationMeta); - this.#saveIndex(this.rootMeta, this.indexMeta); - this.#saveCss(this.rootMeta, this.cssMeta); + this.#saveNavigation(rootMeta, this.navigationMeta); + this.#saveIndex(rootMeta, this.indexMeta); + this.#saveCss(rootMeta, this.cssMeta); } - #saveNavigationInner(meta: NoteMeta) { + #saveNavigationInner(rootMeta: NoteMeta, meta: NoteMeta) { let html = "
  • "; const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`); if (meta.dataFileName && meta.noteId) { - const targetUrl = this.getNoteTargetUrl(meta.noteId, this.rootMeta); + const targetUrl = this.getNoteTargetUrl(meta.noteId, rootMeta); html += `${escapedTitle}`; } else { @@ -99,7 +96,7 @@ export default class HtmlExportProvider extends ZipExportProvider { html += "
      "; for (const child of meta.children) { - html += this.#saveNavigationInner(child); + html += this.#saveNavigationInner(rootMeta, child); } html += "
    "; @@ -119,7 +116,7 @@ export default class HtmlExportProvider extends ZipExportProvider { -
      ${this.#saveNavigationInner(rootMeta)}
    +
      ${this.#saveNavigationInner(rootMeta, rootMeta)}
    `; const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml; diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 2b4ba72e8..abe7be42d 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -1,5 +1,5 @@ import { join } from "path"; -import NoteMeta from "../../meta/note_meta"; +import NoteMeta, { NoteMetaFile } from "../../meta/note_meta"; import { ZipExportProvider } from "./abstract_provider"; import { RESOURCE_DIR } from "../../resource_dir"; import { getResourceDir, isDev } from "../../utils"; @@ -13,7 +13,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { private assetsMeta: NoteMeta[] = []; private indexMeta: NoteMeta | null = null; - prepareMeta(): void { + prepareMeta(metaFile: NoteMetaFile): void { const assets = [ "style.css", "script.js", @@ -32,7 +32,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { dataFileName: asset }; this.assetsMeta.push(assetMeta); - this.metaFile.files.push(assetMeta); + metaFile.files.push(assetMeta); } this.indexMeta = { @@ -40,7 +40,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { dataFileName: "index.html" }; - this.metaFile.files.push(this.indexMeta); + metaFile.files.push(this.indexMeta); } prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer { @@ -58,18 +58,22 @@ export default class ShareThemeExportProvider extends ZipExportProvider { return content; } - afterDone(): void { - this.#saveAssets(this.rootMeta, this.assetsMeta); - this.#saveIndex(); + afterDone(rootMeta: NoteMeta): void { + this.#saveAssets(rootMeta, this.assetsMeta); + this.#saveIndex(rootMeta); } - #saveIndex() { + mapExtension(_type: string | null, _mime: string, _existingExtension: string, _format: string): string | null { + return "html"; + } + + #saveIndex(rootMeta: NoteMeta) { if (!this.indexMeta?.dataFileName) { return; } const note = this.branch.getNote(); - const fullHtml = this.prepareContent(this.rootMeta.title ?? "", note.getContent(), this.rootMeta, note, this.branch); + const fullHtml = this.prepareContent(rootMeta.title ?? "", note.getContent(), rootMeta, note, this.branch); this.archive.append(fullHtml, { name: this.indexMeta.dataFileName }); } From 61dbc15fc6e721d21e31a8e8d8d5304da9c291cc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 20:14:13 +0300 Subject: [PATCH 020/250] feat(export/share): use translation --- apps/client/src/translations/en/translation.json | 4 ++-- apps/client/src/widgets/dialogs/export.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 6d3ad07a2..dd02af8ef 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -110,7 +110,8 @@ "export_status": "Export status", "export_in_progress": "Export in progress: {{progressCount}}", "export_finished_successfully": "Export finished successfully.", - "format_pdf": "PDF - for printing or sharing purposes." + "format_pdf": "PDF - for printing or sharing purposes.", + "share-format": "HTML for web publishing - uses the same theme that is used shared notes, but can be published as a static website." }, "help": { "fullDocumentation": "Help (full documentation is available online)", @@ -1197,7 +1198,6 @@ "restore_provider": "Restore provider to search", "similarity_threshold": "Similarity Threshold", "similarity_threshold_description": "Minimum similarity score (0-1) for notes to be included in context for LLM queries", - "reprocess_index": "Rebuild Search Index", "reprocessing_index": "Rebuilding...", "reprocess_index_started": "Search index optimization started in the background", diff --git a/apps/client/src/widgets/dialogs/export.ts b/apps/client/src/widgets/dialogs/export.ts index ccc748d01..edf9a80dd 100644 --- a/apps/client/src/widgets/dialogs/export.ts +++ b/apps/client/src/widgets/dialogs/export.ts @@ -89,7 +89,7 @@ const TPL = /*html*/`
  • From 9bc966491dfdc6d9ed98e9ae45afe48213c1588b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 21:22:45 +0300 Subject: [PATCH 021/250] fix(edit-docs): import error --- apps/edit-docs/src/edit-docs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edit-docs/src/edit-docs.ts b/apps/edit-docs/src/edit-docs.ts index 940f89540..db5d4be0c 100644 --- a/apps/edit-docs/src/edit-docs.ts +++ b/apps/edit-docs/src/edit-docs.ts @@ -6,7 +6,7 @@ import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js import debounce from "@triliumnext/client/src/services/debounce.js"; import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js"; import cls from "@triliumnext/server/src/services/cls.js"; -import type { AdvancedExportOptions } from "@triliumnext/server/src/services/export/zip.js"; +import type { AdvancedExportOptions } from "@triliumnext/server/src/services/export/zip/abstract_provider.js"; import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js"; import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js"; From 413137ac6437b765ae0cda75ef5fda842f3a9412 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 23 Jun 2025 21:23:44 +0300 Subject: [PATCH 022/250] chore(nx): sync tsconfig --- apps/server/tsconfig.app.json | 3 +++ apps/server/tsconfig.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/server/tsconfig.app.json b/apps/server/tsconfig.app.json index 61f4a77fe..eb7f102aa 100644 --- a/apps/server/tsconfig.app.json +++ b/apps/server/tsconfig.app.json @@ -34,6 +34,9 @@ "src/**/*.spec.jsx" ], "references": [ + { + "path": "../../packages/ckeditor5/tsconfig.lib.json" + }, { "path": "../../packages/turndown-plugin-gfm/tsconfig.lib.json" }, diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index baacd3fa5..6bc224295 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -3,6 +3,9 @@ "files": [], "include": [], "references": [ + { + "path": "../../packages/ckeditor5" + }, { "path": "../../packages/turndown-plugin-gfm" }, From a2110ca631a0bf35424a7fd568d6d82cb72939bd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 24 Jun 2025 17:44:47 +0300 Subject: [PATCH 023/250] fix(export/share): tree not expanding properly --- .../src/services/export/zip/share_theme.ts | 4 ++-- apps/server/src/share/content_renderer.ts | 19 +++++++++++++++---- apps/server/src/share/routes.ts | 1 + packages/share-theme/src/templates/page.ejs | 11 +---------- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index abe7be42d..59746626f 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -1,6 +1,6 @@ import { join } from "path"; import NoteMeta, { NoteMetaFile } from "../../meta/note_meta"; -import { ZipExportProvider } from "./abstract_provider"; +import { ZipExportProvider } from "./abstract_provider.js"; import { RESOURCE_DIR } from "../../resource_dir"; import { getResourceDir, isDev } from "../../utils"; import fs from "fs"; @@ -50,7 +50,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { const basePath = "../".repeat(noteMeta.notePath.length - 1); if (note) { - content = renderNoteForExport(note, branch, basePath); + content = renderNoteForExport(note, branch, basePath, noteMeta.notePath.slice(0, -1)); content = content.replace(/href="[^"]*\.\/([a-zA-Z0-9_\/]{12})[^"]*"/g, "href=\"#root/$1\""); content = this.rewriteFn(content, noteMeta); } diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 041e5cc17..62c9df71f 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -61,7 +61,7 @@ function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot { return getSharedSubTreeRoot(parentBranch.getParentNote()); } -export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath: string) { +export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath: string, ancestors: string[]) { const subRoot: Subroot = { branch: parentBranch, note: parentBranch.getNote() @@ -69,7 +69,7 @@ export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath return renderNoteContentInternal(note, { subRoot, - rootNoteId: note.getParentNotes()[0].noteId, + rootNoteId: parentBranch.noteId, cssToLoad: [ `${basePath}style.css`, `${basePath}boxicons.css` @@ -77,13 +77,22 @@ export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath jsToLoad: [ `${basePath}script.js` ], - logoUrl: `${basePath}icon-color.svg` + logoUrl: `${basePath}icon-color.svg`, + ancestors }); } export function renderNoteContent(note: SNote) { const subRoot = getSharedSubTreeRoot(note); + const ancestors: string[] = []; + let notePointer = note; + while (notePointer.parents[0].noteId !== subRoot.note?.noteId) { + const pointerParent = notePointer.parents[0]; + ancestors.push(pointerParent.noteId); + notePointer = pointerParent; + } + // Determine CSS to load. const cssToLoad: string[] = []; if (!isDev && !note.isLabelTruthy("shareOmitDefaultCss")) { @@ -110,7 +119,8 @@ export function renderNoteContent(note: SNote) { rootNoteId: "_share", cssToLoad, jsToLoad, - logoUrl + logoUrl, + ancestors }); } @@ -120,6 +130,7 @@ interface RenderArgs { cssToLoad: string[]; jsToLoad: string[]; logoUrl: string; + ancestors: string[]; } function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) { diff --git a/apps/server/src/share/routes.ts b/apps/server/src/share/routes.ts index 4b0281ec5..ceaeedb1b 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -137,6 +137,7 @@ function register(router: Router) { return; } + res.send(renderNoteContent(note)); } diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index 243f788a1..10a6c474a 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -97,16 +97,7 @@ content = content.replaceAll(headingRe, (...match) => { <% if (hasTree) { %> <% } %> From bc4643fed2ed2937c501faf034fb59a31f0a2a65 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 24 Jun 2025 17:48:52 +0300 Subject: [PATCH 024/250] refactor(share): use internal rendering method for subtemplates --- apps/server/src/share/content_renderer.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 62c9df71f..529db2116 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -185,14 +185,18 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) // Render with the default view otherwise. const templatePath = join(getResourceDir(), "share-theme", "templates", "page.ejs"); - return ejs.render(readFileSync(templatePath, "utf-8"), opts, { + return ejs.render(readTemplate(templatePath), opts, { includer: (path) => { const templatePath = join(getResourceDir(), "share-theme", "templates", `${path}.ejs`); - return { filename: templatePath } + return { template: readTemplate(templatePath) }; } }); } +function readTemplate(path: string) { + return readFileSync(path, "utf-8"); +} + function getContent(note: SNote | BNote) { if (note.isProtected) { return { From 3a55490bbf859912648aabdcb7d8250a904c5720 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 24 Jun 2025 18:08:29 +0300 Subject: [PATCH 025/250] refactor(share): use a string cache for templates --- apps/server/src/services/export/zip/share_theme.ts | 6 ++++-- apps/server/src/share/content_renderer.ts | 10 +++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 59746626f..efde0b5f5 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -51,8 +51,10 @@ export default class ShareThemeExportProvider extends ZipExportProvider { if (note) { content = renderNoteForExport(note, branch, basePath, noteMeta.notePath.slice(0, -1)); - content = content.replace(/href="[^"]*\.\/([a-zA-Z0-9_\/]{12})[^"]*"/g, "href=\"#root/$1\""); - content = this.rewriteFn(content, noteMeta); + if (typeof content === "string") { + content = content.replace(/href="[^"]*\.\/([a-zA-Z0-9_\/]{12})[^"]*"/g, "href=\"#root/$1\""); + content = this.rewriteFn(content, noteMeta); + } } return content; diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 529db2116..d5c3a4c9e 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -18,6 +18,7 @@ import { readFileSync } from "fs"; const shareAdjustedAssetPath = isDev ? assetPath : `../${assetPath}`; const shareAdjustedAppPath = isDev ? app_path : `../${app_path}`; +const templateCache: Map = new Map(); /** * Represents the output of the content renderer. @@ -194,7 +195,14 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) } function readTemplate(path: string) { - return readFileSync(path, "utf-8"); + const cachedTemplate = templateCache.get(path); + if (cachedTemplate) { + return cachedTemplate; + } + + const templateString = readFileSync(path, "utf-8"); + templateCache.set(path, templateString); + return templateString; } function getContent(note: SNote | BNote) { From 6d446c5b275ea116b7d65b88011328de3f3570f8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 24 Jun 2025 18:49:11 +0300 Subject: [PATCH 026/250] fix(export/share): asset path in prod --- apps/server/src/services/export/zip/share_theme.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index efde0b5f5..f8475b0c2 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -105,10 +105,8 @@ function getShareThemeAssets(nameWithExtension: string) { path = join(RESOURCE_DIR, "images", nameWithExtension); } else if (isDev) { path = join(getResourceDir(), "..", "..", "client", "dist", "src", nameWithExtension); - } - - if (!path) { - throw new Error("Not yet defined."); + } else { + path = join(getResourceDir(), "public", "src", nameWithExtension); } return fs.readFileSync(path); From 3ebfee8bd2c3871c622b30e0fbdab1a96d4faffd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 24 Jun 2025 18:49:19 +0300 Subject: [PATCH 027/250] fix(export/share): tree error in prod --- apps/server/src/share/content_renderer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index d5c3a4c9e..167bdb716 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -88,8 +88,11 @@ export function renderNoteContent(note: SNote) { const ancestors: string[] = []; let notePointer = note; - while (notePointer.parents[0].noteId !== subRoot.note?.noteId) { + while (notePointer.parents[0]?.noteId !== subRoot.note?.noteId) { const pointerParent = notePointer.parents[0]; + if (!pointerParent) { + break; + } ancestors.push(pointerParent.noteId); notePointer = pointerParent; } From 9abdbbbc5b2bfd9f6fbf787254eb7d32d6fb43a2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 24 Jun 2025 19:06:18 +0300 Subject: [PATCH 028/250] refactor(export/share): fix type --- apps/server/src/services/export/zip/markdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/services/export/zip/markdown.ts b/apps/server/src/services/export/zip/markdown.ts index 1ace2051a..827f059d6 100644 --- a/apps/server/src/services/export/zip/markdown.ts +++ b/apps/server/src/services/export/zip/markdown.ts @@ -1,5 +1,5 @@ import NoteMeta from "../../meta/note_meta" -import { ZipExportProvider } from "./abstract_provider" +import { ZipExportProvider } from "./abstract_provider.js" import mdService from "../markdown.js"; export default class MarkdownExportProvider extends ZipExportProvider { From 06de06b50115df24bb54ee1a9f6b376bdd6cff1e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 24 Jun 2025 19:21:09 +0300 Subject: [PATCH 029/250] refactor(export/share): share type for format --- apps/edit-docs/src/edit-docs.ts | 4 ++-- apps/server/src/etapi/notes.ts | 3 ++- apps/server/src/services/export/single.ts | 5 +++-- apps/server/src/services/export/zip.ts | 6 +++--- apps/server/src/services/export/zip/abstract_provider.ts | 4 +++- apps/server/src/services/export/zip/share_theme.ts | 4 ---- apps/server/src/services/meta/note_meta.ts | 3 ++- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/edit-docs/src/edit-docs.ts b/apps/edit-docs/src/edit-docs.ts index db5d4be0c..b6a04969f 100644 --- a/apps/edit-docs/src/edit-docs.ts +++ b/apps/edit-docs/src/edit-docs.ts @@ -6,7 +6,7 @@ import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js import debounce from "@triliumnext/client/src/services/debounce.js"; import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js"; import cls from "@triliumnext/server/src/services/cls.js"; -import type { AdvancedExportOptions } from "@triliumnext/server/src/services/export/zip/abstract_provider.js"; +import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/server/src/services/export/zip/abstract_provider.js"; import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js"; import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js"; @@ -75,7 +75,7 @@ async function setOptions() { optionsService.setOption("compressImages", "false"); } -async function exportData(noteId: string, format: "html" | "markdown", outputPath: string, ignoredFiles?: Set) { +async function exportData(noteId: string, format: ExportFormat, outputPath: string, ignoredFiles?: Set) { const zipFilePath = "output.zip"; try { diff --git a/apps/server/src/etapi/notes.ts b/apps/server/src/etapi/notes.ts index 82280d0b9..941d09566 100644 --- a/apps/server/src/etapi/notes.ts +++ b/apps/server/src/etapi/notes.ts @@ -14,6 +14,7 @@ import type { ParsedQs } from "qs"; import type { NoteParams } from "../services/note-interface.js"; import type { SearchParams } from "../services/search/services/types.js"; import type { ValidatorMap } from "./etapi-interface.js"; +import type { ExportFormat } from "../services/export/zip/abstract_provider.js"; function register(router: Router) { eu.route(router, "get", "/etapi/notes", (req, res, next) => { @@ -157,7 +158,7 @@ function register(router: Router) { // (e.g. branchIds are not seen in UI), that we export "note export" instead. const branch = note.getParentBranches()[0]; - zipExportService.exportToZip(taskContext, branch, format as "html" | "markdown", res); + zipExportService.exportToZip(taskContext, branch, format as ExportFormat, res); }); eu.route(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => { diff --git a/apps/server/src/services/export/single.ts b/apps/server/src/services/export/single.ts index b626bf919..2748c8850 100644 --- a/apps/server/src/services/export/single.ts +++ b/apps/server/src/services/export/single.ts @@ -9,8 +9,9 @@ import type TaskContext from "../task_context.js"; import type BBranch from "../../becca/entities/bbranch.js"; import type { Response } from "express"; import type BNote from "../../becca/entities/bnote.js"; +import type { ExportFormat } from "./zip/abstract_provider.js"; -function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response) { +function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: ExportFormat, res: Response) { const note = branch.getNote(); if (note.type === "image" || note.type === "file") { @@ -33,7 +34,7 @@ function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "ht taskContext.taskSucceeded(); } -export function mapByNoteType(note: BNote, content: string | Buffer, format: "html" | "markdown") { +export function mapByNoteType(note: BNote, content: string | Buffer, format: ExportFormat) { let payload, extension, mime; if (typeof content !== "string") { diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 26af3424f..1bfc1e842 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -19,12 +19,12 @@ import BBranch from "../../becca/entities/bbranch.js"; import type { Response } from "express"; import type { NoteMetaFile } from "../meta/note_meta.js"; import HtmlExportProvider from "./zip/html.js"; -import { AdvancedExportOptions, ZipExportProviderData } from "./zip/abstract_provider.js"; +import { AdvancedExportOptions, type ExportFormat, ZipExportProviderData } from "./zip/abstract_provider.js"; import MarkdownExportProvider from "./zip/markdown.js"; import ShareThemeExportProvider from "./zip/share_theme.js"; import type BNote from "../../becca/entities/bnote.js"; -async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown" | "share", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) { +async function exportToZip(taskContext: TaskContext, branch: BBranch, format: ExportFormat, res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) { if (!["html", "markdown", "share"].includes(format)) { throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`); } @@ -432,7 +432,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h taskContext.taskSucceeded(); } -async function exportToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string, zipExportOptions?: AdvancedExportOptions) { +async function exportToZipFile(noteId: string, format: ExportFormat, zipFilePath: string, zipExportOptions?: AdvancedExportOptions) { const fileOutputStream = fs.createWriteStream(zipFilePath); const taskContext = new TaskContext("no-progress-reporting"); diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts index 6ca5fdb9a..f777ed1cb 100644 --- a/apps/server/src/services/export/zip/abstract_provider.ts +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -6,6 +6,8 @@ import mimeTypes from "mime-types"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; +export type ExportFormat = "html" | "markdown" | "share"; + export interface AdvancedExportOptions { /** * If `true`, then only the note's content will be kept. If `false` (default), then each page will have its own template. @@ -49,7 +51,7 @@ export abstract class ZipExportProvider { abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer; abstract afterDone(rootMeta: NoteMeta): void; - mapExtension(type: string | null, mime: string, existingExtension: string, format: string) { + mapExtension(type: string | null, mime: string, existingExtension: string, format: ExportFormat) { // the following two are handled specifically since we always want to have these extensions no matter the automatic detection // and/or existing detected extensions in the note name if (type === "text" && format === "markdown") { diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index f8475b0c2..03bdb68b4 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -65,10 +65,6 @@ export default class ShareThemeExportProvider extends ZipExportProvider { this.#saveIndex(rootMeta); } - mapExtension(_type: string | null, _mime: string, _existingExtension: string, _format: string): string | null { - return "html"; - } - #saveIndex(rootMeta: NoteMeta) { if (!this.indexMeta?.dataFileName) { return; diff --git a/apps/server/src/services/meta/note_meta.ts b/apps/server/src/services/meta/note_meta.ts index 33e7a7843..7a7a9f4b7 100644 --- a/apps/server/src/services/meta/note_meta.ts +++ b/apps/server/src/services/meta/note_meta.ts @@ -1,6 +1,7 @@ import type { NoteType } from "@triliumnext/commons"; import type AttachmentMeta from "./attachment_meta.js"; import type AttributeMeta from "./attribute_meta.js"; +import type { ExportFormat } from "../export/zip/abstract_provider.js"; export interface NoteMetaFile { formatVersion: number; @@ -19,7 +20,7 @@ export default interface NoteMeta { type?: NoteType; mime?: string; /** 'html' or 'markdown', applicable to text notes only */ - format?: "html" | "markdown"; + format?: ExportFormat; dataFileName?: string; dirFileName?: string; /** this file should not be imported (e.g., HTML navigation) */ From fded714f18ff108f8710080a09180f47f79388a8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 24 Jun 2025 19:53:21 +0300 Subject: [PATCH 030/250] fix(export/share): use right extension for images --- apps/server/src/services/export/zip.ts | 3 ++- .../src/services/export/zip/abstract_provider.ts | 12 +++++++++++- apps/server/src/services/export/zip/share_theme.ts | 10 +++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 1bfc1e842..10b84abce 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -23,6 +23,7 @@ import { AdvancedExportOptions, type ExportFormat, ZipExportProviderData } from import MarkdownExportProvider from "./zip/markdown.js"; import ShareThemeExportProvider from "./zip/share_theme.js"; import type BNote from "../../becca/entities/bnote.js"; +import { NoteType } from "@triliumnext/commons"; async function exportToZip(taskContext: TaskContext, branch: BBranch, format: ExportFormat, res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) { if (!["html", "markdown", "share"].includes(format)) { @@ -77,7 +78,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: Ex } } - function getDataFileName(type: string | null, mime: string, baseFileName: string, existingFileNames: Record): string { + function getDataFileName(type: NoteType | null, mime: string, baseFileName: string, existingFileNames: Record): string { let fileName = baseFileName.trim(); // Crop fileName to avoid its length exceeding 30 and prevent cutting into the extension. diff --git a/apps/server/src/services/export/zip/abstract_provider.ts b/apps/server/src/services/export/zip/abstract_provider.ts index f777ed1cb..c9645a843 100644 --- a/apps/server/src/services/export/zip/abstract_provider.ts +++ b/apps/server/src/services/export/zip/abstract_provider.ts @@ -3,6 +3,7 @@ import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js" import type BNote from "../../../becca/entities/bnote.js"; import type BBranch from "../../../becca/entities/bbranch.js"; import mimeTypes from "mime-types"; +import { NoteType } from "@triliumnext/commons"; type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string; @@ -51,7 +52,16 @@ export abstract class ZipExportProvider { abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer; abstract afterDone(rootMeta: NoteMeta): void; - mapExtension(type: string | null, mime: string, existingExtension: string, format: ExportFormat) { + /** + * Determines the extension of the resulting file for a specific note type. + * + * @param type the type of the note. + * @param mime the mime type of the note. + * @param existingExtension the existing extension, including the leading period character. + * @param format the format requested for export (e.g. HTML, Markdown). + * @returns an extension *without* the leading period character, or `null` to preserve the existing extension instead. + */ + mapExtension(type: NoteType | null, mime: string, existingExtension: string, format: ExportFormat) { // the following two are handled specifically since we always want to have these extensions no matter the automatic detection // and/or existing detected extensions in the note name if (type === "text" && format === "markdown") { diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 03bdb68b4..06609b031 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -1,6 +1,6 @@ import { join } from "path"; import NoteMeta, { NoteMetaFile } from "../../meta/note_meta"; -import { ZipExportProvider } from "./abstract_provider.js"; +import { ExportFormat, ZipExportProvider } from "./abstract_provider.js"; import { RESOURCE_DIR } from "../../resource_dir"; import { getResourceDir, isDev } from "../../utils"; import fs from "fs"; @@ -65,6 +65,14 @@ export default class ShareThemeExportProvider extends ZipExportProvider { this.#saveIndex(rootMeta); } + mapExtension(type: string | null, mime: string, existingExtension: string, format: ExportFormat): string | null { + if (mime.startsWith("image/")) { + return null; + } + + return "html"; + } + #saveIndex(rootMeta: NoteMeta) { if (!this.indexMeta?.dataFileName) { return; From 9cf7fa1997ec754b4854e4c1a5f9eacd1277710d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 24 Jun 2025 22:14:15 +0300 Subject: [PATCH 031/250] fix(export/share): use right extension for clones --- apps/server/src/services/export/zip.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 10b84abce..58df003af 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -126,7 +126,8 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: Ex const notePath = parentMeta.notePath.concat([note.noteId]); if (note.noteId in noteIdToMeta) { - const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${format === "html" ? "html" : "md"}`); + const extension = provider.mapExtension("text", "text/html", "", format); + const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${extension}`); const meta: NoteMeta = { isClone: true, From c4e2c003de271a025b2a8143476261e94ad9ebda Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 21 Oct 2025 21:35:05 +0300 Subject: [PATCH 032/250] feat(i18n): enable Italian language --- apps/client/src/widgets/collections/calendar/index.tsx | 1 + apps/server/src/services/i18n.ts | 1 + packages/commons/src/lib/i18n.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/apps/client/src/widgets/collections/calendar/index.tsx b/apps/client/src/widgets/collections/calendar/index.tsx index 2d7d77b2e..3c3925bae 100644 --- a/apps/client/src/widgets/collections/calendar/index.tsx +++ b/apps/client/src/widgets/collections/calendar/index.tsx @@ -66,6 +66,7 @@ export const LOCALE_MAPPINGS: Record Promise<{ de de: () => import("@fullcalendar/core/locales/de"), es: () => import("@fullcalendar/core/locales/es"), fr: () => import("@fullcalendar/core/locales/fr"), + it: () => import("@fullcalendar/core/locales/it"), cn: () => import("@fullcalendar/core/locales/zh-cn"), tw: () => import("@fullcalendar/core/locales/zh-tw"), ro: () => import("@fullcalendar/core/locales/ro"), diff --git a/apps/server/src/services/i18n.ts b/apps/server/src/services/i18n.ts index a23ff7ea2..be7ebdeb5 100644 --- a/apps/server/src/services/i18n.ts +++ b/apps/server/src/services/i18n.ts @@ -16,6 +16,7 @@ export const DAYJS_LOADER: Record Promise import("dayjs/locale/es.js"), "fa": () => import("dayjs/locale/fa.js"), "fr": () => import("dayjs/locale/fr.js"), + "it": () => import("dayjs/locale/it.js"), "he": () => import("dayjs/locale/he.js"), "ja": () => import("dayjs/locale/ja.js"), "ku": () => import("dayjs/locale/ku.js"), diff --git a/packages/commons/src/lib/i18n.ts b/packages/commons/src/lib/i18n.ts index fc7c51577..65ff196d2 100644 --- a/packages/commons/src/lib/i18n.ts +++ b/packages/commons/src/lib/i18n.ts @@ -17,6 +17,7 @@ const UNSORTED_LOCALES = [ { id: "en", name: "English", electronLocale: "en" }, { id: "es", name: "Español", electronLocale: "es" }, { id: "fr", name: "Français", electronLocale: "fr" }, + { id: "it", name: "Italiano", electronLocale: "it" }, { id: "ja", name: "日本語", electronLocale: "ja" }, { id: "pt_br", name: "Português (Brasil)", electronLocale: "pt_BR" }, { id: "pt", name: "Português (Portugal)", electronLocale: "pt_PT" }, From 8590ff1f466d2c6bbaed425810b7ef3cfb2b5c75 Mon Sep 17 00:00:00 2001 From: Adorian Doran Date: Wed, 22 Oct 2025 00:45:51 +0300 Subject: [PATCH 033/250] style/text editor/reference links color: fix regression --- apps/client/src/stylesheets/theme-dark.css | 6 ++++++ apps/client/src/stylesheets/theme-light.css | 5 +++++ apps/client/src/stylesheets/theme-next-dark.css | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/apps/client/src/stylesheets/theme-dark.css b/apps/client/src/stylesheets/theme-dark.css index 0a622d97d..a50a6e6c2 100644 --- a/apps/client/src/stylesheets/theme-dark.css +++ b/apps/client/src/stylesheets/theme-dark.css @@ -86,6 +86,11 @@ body ::-webkit-calendar-picker-indicator { --custom-color: var(--dark-theme-custom-color); } +.ck-content a.reference-link, +.ck-content a.reference-link > span { + color: var(--dark-theme-custom-color, inherit); +} + .excalidraw.theme--dark { --theme-filter: invert(80%) hue-rotate(180deg) !important; } @@ -101,3 +106,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before { .ck-content pre { box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6) !important; } + diff --git a/apps/client/src/stylesheets/theme-light.css b/apps/client/src/stylesheets/theme-light.css index a07b9799f..630304102 100644 --- a/apps/client/src/stylesheets/theme-light.css +++ b/apps/client/src/stylesheets/theme-light.css @@ -84,4 +84,9 @@ html { #left-pane .fancytree-node.tinted { --custom-color: var(--light-theme-custom-color); +} + +.ck-content a.reference-link, +.ck-content a.reference-link > span { + color: var(--light-theme-custom-color, inherit); } \ No newline at end of file diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index 8b15e7676..12bc4f0f1 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -277,6 +277,11 @@ --custom-bg-color: hsl(var(--custom-color-hue), 20%, 33%, 0.4); } +.ck-content a.reference-link, +.ck-content a.reference-link > span { + color: var(--dark-theme-custom-color, inherit); +} + body ::-webkit-calendar-picker-indicator { filter: invert(1); } From 4b34ae3fd45b8090242631142ff2a441faecf79a Mon Sep 17 00:00:00 2001 From: Adorian Doran Date: Wed, 22 Oct 2025 00:51:58 +0300 Subject: [PATCH 034/250] style/text editor/reference links: make the underline use the same color as the link caption --- .../src/stylesheets/theme-next/notes/text.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/client/src/stylesheets/theme-next/notes/text.css b/apps/client/src/stylesheets/theme-next/notes/text.css index 0cbf00098..70517ef2e 100644 --- a/apps/client/src/stylesheets/theme-next/notes/text.css +++ b/apps/client/src/stylesheets/theme-next/notes/text.css @@ -666,4 +666,17 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child { .ck-content .table > figcaption { background: var(--accented-background-color); color: var(--main-text-color); +} + +/* Reference link */ + +.ck-content a.reference-link, +.ck-content a.reference-link:hover { + /* Apply underline only to the span inside the link so it can follow the + * target note's user defined color */ + text-decoration: none; +} + +.ck-content a.reference-link > span { + text-decoration: underline; } \ No newline at end of file From 4bcf209072beb9ce7315b262cc7659f64bf92287 Mon Sep 17 00:00:00 2001 From: Adorian Doran Date: Wed, 22 Oct 2025 00:57:55 +0300 Subject: [PATCH 035/250] style/reference links: enable colors in table collections as well --- apps/client/src/stylesheets/theme-dark.css | 2 +- apps/client/src/stylesheets/theme-light.css | 2 +- apps/client/src/stylesheets/theme-next-dark.css | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/src/stylesheets/theme-dark.css b/apps/client/src/stylesheets/theme-dark.css index a50a6e6c2..2d08163d6 100644 --- a/apps/client/src/stylesheets/theme-dark.css +++ b/apps/client/src/stylesheets/theme-dark.css @@ -86,7 +86,7 @@ body ::-webkit-calendar-picker-indicator { --custom-color: var(--dark-theme-custom-color); } -.ck-content a.reference-link, +.reference-link, .ck-content a.reference-link > span { color: var(--dark-theme-custom-color, inherit); } diff --git a/apps/client/src/stylesheets/theme-light.css b/apps/client/src/stylesheets/theme-light.css index 630304102..3bf49341c 100644 --- a/apps/client/src/stylesheets/theme-light.css +++ b/apps/client/src/stylesheets/theme-light.css @@ -86,7 +86,7 @@ html { --custom-color: var(--light-theme-custom-color); } -.ck-content a.reference-link, +.reference-link, .ck-content a.reference-link > span { color: var(--light-theme-custom-color, inherit); } \ No newline at end of file diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index 12bc4f0f1..5b012996e 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -277,7 +277,7 @@ --custom-bg-color: hsl(var(--custom-color-hue), 20%, 33%, 0.4); } -.ck-content a.reference-link, +.reference-link, .ck-content a.reference-link > span { color: var(--dark-theme-custom-color, inherit); } From d4b05fa0a01015ee24f4d60f78d10098cfdaf37e Mon Sep 17 00:00:00 2001 From: Adorian Doran Date: Wed, 22 Oct 2025 01:29:54 +0300 Subject: [PATCH 036/250] style/dialogs/delete confirmation: fix the spacing of the note list --- apps/client/src/stylesheets/theme-next/dialogs.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/client/src/stylesheets/theme-next/dialogs.css b/apps/client/src/stylesheets/theme-next/dialogs.css index c3b1d5238..ed617991c 100644 --- a/apps/client/src/stylesheets/theme-next/dialogs.css +++ b/apps/client/src/stylesheets/theme-next/dialogs.css @@ -392,7 +392,8 @@ div.tn-tool-dialog { } .delete-notes-list .note-path { - padding-inline-end: 8px; + padding-inline-start: 8px; + color: var(--muted-text-color) } /* From 1d28a5e5b8461f38ed9bcc634f155e8c4900b844 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 21 Oct 2025 23:27:49 +0200 Subject: [PATCH 037/250] Update translation files Updated by "Cleanup translation files" add-on in Weblate. Translation: Trilium Notes/README Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ --- docs/README-fr.md | 155 +++++++++++++++++++++++++--------------------- 1 file changed, 83 insertions(+), 72 deletions(-) diff --git a/docs/README-fr.md b/docs/README-fr.md index 0669eafff..9d138830a 100644 --- a/docs/README-fr.md +++ b/docs/README-fr.md @@ -175,55 +175,61 @@ features, suggestions, or issues you may have! ### Windows / MacOS -Download the binary release for your platform from the [latest release -page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package -and run the `trilium` executable. +Téléchargez la version binaire pour votre plateforme à partir de la [dernière +page de version](https://github.com/TriliumNext/Trilium/releases/latest), +décompressez le package et exécutez l'exécutable `trilium`. ### Linux Si votre distribution est répertoriée dans le tableau ci-dessous, utilisez le package de votre distribution. -[![Packaging -status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions) +[![État du +Packaging](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions) -You may also download the binary release for your platform from the [latest -release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the -package and run the `trilium` executable. +Vous pouvez également télécharger la version binaire pour votre plateforme à +partir de la [dernière page de +version](https://github.com/TriliumNext/Trilium/releases/latest), décompresser +le package et lancer l'exécutable `trilium`. -TriliumNext is also provided as a Flatpak, but not yet published on FlatHub. +TriliumNext est également fourni sous forme de Flatpak, mais pas encore publié +sur FlatHub. -### Browser (any OS) +### Navigateur (tout système d'exploitation) -If you use a server installation (see below), you can directly access the web -interface (which is almost identical to the desktop app). +Si vous utilisez une installation serveur (voir ci-dessous), vous pouvez accéder +directement à l'interface Web (qui est presque identique à l'application de +bureau). -Currently only the latest versions of Chrome & Firefox are supported (and -tested). +Actuellement, seules les dernières versions de Chrome & Firefox sont supportées +(et testées). ### Mobile -To use TriliumNext on a mobile device, you can use a mobile web browser to -access the mobile interface of a server installation (see below). +Pour utiliser TriliumNext sur un appareil mobile, vous pouvez utiliser un +navigateur Web afin d' accéder à l'interface d'une installation serveur (voir +ci-dessous). -See issue https://github.com/TriliumNext/Trilium/issues/4962 for more -information on mobile app support. +Pour plus d’informations sur le support de l’application mobile, consultez le +ticket https://github.com/TriliumNext/Trilium/issues/4962. -If you prefer a native Android app, you can use +Si vous préférez une application Android native, vous pouvez utiliser [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). -Report bugs and missing features at [their -repository](https://github.com/FliegendeWurst/TriliumDroid). Note: It is best to -disable automatic updates on your server installation (see below) when using -TriliumDroid since the sync version must match between Trilium and TriliumDroid. +Signalez les bugs et les fonctionnalités manquantes sur [leur +dépôt](https://github.com/FliegendeWurst/TriliumDroid). Remarque : Il est +préférable de désactiver les mises à jour automatiques sur votre serveur (voir +ci-dessous) lorsque vous utilisez TriliumDroid, car les versions doivent rester +synchronisées entre Trilium et TriliumDroid. -### Server +### Serveur -To install TriliumNext on your own server (including via Docker from -[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server -installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation). +Pour installer TriliumNext sur votre propre serveur (y compris via Docker depuis +[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)), suivez [les +documents d'installation du +serveur](https://triliumnext.github.io/Docs/Wiki/server-installation). -## 💻 Contribute +## 💻 Contribuer ### Translations @@ -277,60 +283,65 @@ guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/D for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above. -## 👏 Shoutouts +## 👏 Dédicaces -* [zadam](https://github.com/zadam) for the original concept and implementation - of the application. -* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the - application icon. -* [nriver](https://github.com/nriver) for his work on internationalization. -* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas. -* [antoniotejada](https://github.com/nriver) for the original syntax highlight - widget. -* [Dosu](https://dosu.dev/) for providing us with the automated responses to - GitHub issues and discussions. -* [Tabler Icons](https://tabler.io/icons) for the system tray icons. +* [zadam](https://github.com/zadam) pour le concept original et la mise en œuvre + de l'application. +* [Sarah Hussein](https://github.com/Sarah-Hussein) pour la conception de + l'icône de l'application. +* [nriver](https://github.com/nriver) pour son travail sur + l’internationalisation. +* [Thomas Frei](https://github.com/thfrei) pour son travail original sur le + Canvas. +* [antoniotejada](https://github.com/nriver) pour le widget de coloration + syntaxique original. +* [Dosu](https://dosu.dev/) pour nous avoir fourni des réponses automatisées aux + problèmes et aux discussions sur GitHub. +* [Tabler Icons](https://tabler.io/icons) pour les icônes de la barre d'état + système. -Trilium would not be possible without the technologies behind it: +Trilium ne serait pas possible sans les technologies qui le sous-tendent : -* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind - text notes. We are grateful for being offered a set of the premium features. -* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with - support for huge amount of languages. -* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite - whiteboard used in Canvas notes. -* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the - mind map functionality. -* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical - maps. -* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive - table used in collections. -* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library - without real competition. -* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library. - Used in [relation - maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link - maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map) +* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - est l’éditeur visuel + utilisé pour les notes textuelles. Nous remercions l’équipe pour la mise à + disposition d’un ensemble de fonctionnalités premium. +* [CodeMirror](https://github.com/codemirror/CodeMirror) - éditeur de code + prenant en charge un grand nombre de langages. +* [Excalidraw](https://github.com/excalidraw/excalidraw) - le tableau blanc + infini utilisé dans les notes Canvas. +* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - fournit la + fonctionnalité de carte mentale. +* [Leaflet](https://github.com/Leaflet/Leaflet) - pour le rendu des cartes + géographiques. +* [Tabulator](https://github.com/olifolkerd/tabulator) - pour le tableau + interactif utilisé dans les collections. +* [FancyTree](https://github.com/mar10/fancytree) - bibliothèque d'arborescence + riche en fonctionnalités sans réelle concurrence. +* [jsPlumb](https://github.com/jsplumb/jsplumb) - Bibliothèque de connectivité + visuelle. Utilisée dans les [cartes de + relations](https://triliumnext.github.io/Docs/Wiki/relation-map.html) et les + [cartes de + liens](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map) ## 🤝 Support -Trilium is built and maintained with [hundreds of hours of -work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your -support keeps it open-source, improves features, and covers costs such as -hosting. +Trilium est développé et maintenu grâce à [des centaines d'heures de +travail](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Votre +soutien permet son maintien en open-source, d'améliorer ses fonctionnalités et +de couvrir des coûts tels que l'hébergement. -Consider supporting the main developer -([eliandoran](https://github.com/eliandoran)) of the application via: +Envisagez de soutenir le développeur principal +([eliandoran](https://github.com/eliandoran)) de l'application via : -- [GitHub Sponsors](https://github.com/sponsors/eliandoran) +- [Sponsors GitHub](https://github.com/sponsors/eliandoran) - [PayPal](https://paypal.me/eliandoran) -- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran) +- [Offrez-moi un café](https://buymeacoffee.com/eliandoran) ## 🔑 License -Copyright 2017-2025 zadam, Elian Doran, and other contributors +Copyright 2017-2025 zadam, Elian Doran et autres contributeurs -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU Affero General Public License as published by the Free -Software Foundation, either version 3 of the License, or (at your option) any -later version. +Ce programme est un logiciel libre : vous pouvez le redistribuer et/ou le +modifier selon les termes de la licence publique générale GNU Affero telle que +publiée par la Free Software Foundation, soit la version 3 de la licence, soit +(à votre choix) toute version ultérieure. From a6fce1b4c8fb567c5477993bdc30eec270ead4f7 Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 21 Oct 2025 23:53:29 +0200 Subject: [PATCH 038/250] Translated using Weblate (French) Currently translated at 86.9% (1409 of 1621 strings) Translation: Trilium Notes/Client Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/ --- .../src/translations/fr/translation.json | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json index 5a5518a0b..9d97cd345 100644 --- a/apps/client/src/translations/fr/translation.json +++ b/apps/client/src/translations/fr/translation.json @@ -184,7 +184,8 @@ }, "import-status": "Statut de l'importation", "in-progress": "Importation en cours : {{progress}}", - "successful": "Importation terminée avec succès." + "successful": "Importation terminée avec succès.", + "importZipRecommendation": "Lors de l'importation d'un fichier ZIP, la hiérarchie des notes reflétera la structure des sous-répertoires au sein de l'archive." }, "include_note": { "dialog_title": "Inclure une note", @@ -1160,7 +1161,8 @@ "download_images_description": "Le HTML collé peut contenir des références à des images en ligne, Trilium trouvera ces références et téléchargera les images afin qu'elles soient disponibles hors ligne.", "enable_image_compression": "Activer la compression des images", "max_image_dimensions": "Largeur/hauteur maximale d'une image en pixels (l'image sera redimensionnée si elle dépasse ce paramètre).", - "jpeg_quality_description": "Qualité JPEG (10 - pire qualité, 100 - meilleure qualité, 50 - 85 est recommandé)" + "jpeg_quality_description": "Qualité JPEG (10 - pire qualité, 100 - meilleure qualité, 50 - 85 est recommandé)", + "max_image_dimensions_unit": "pixels" }, "attachment_erasure_timeout": { "attachment_erasure_timeout": "Délai d'effacement des pièces jointes", @@ -1192,7 +1194,8 @@ "note_revisions_snapshot_limit_description": "La limite du nombre de versions de note désigne le nombre maximum de versions pouvant être enregistrées pour chaque note. -1 signifie aucune limite, 0 signifie supprimer toutes les versions. Vous pouvez définir le nombre maximal de versions pour une seule note avec le label #versioningLimit.", "snapshot_number_limit_label": "Nombre limite de versions de note :", "erase_excess_revision_snapshots": "Effacer maintenant les versions en excès", - "erase_excess_revision_snapshots_prompt": "Les versions en excès ont été effacées." + "erase_excess_revision_snapshots_prompt": "Les versions en excès ont été effacées.", + "snapshot_number_limit_unit": "instantanés" }, "search_engine": { "title": "Moteur de recherche", @@ -1234,19 +1237,35 @@ "title": "Table des matières", "description": "La table des matières apparaîtra dans les notes textuelles lorsque la note comporte plus d'un nombre défini de titres. Vous pouvez personnaliser ce nombre :", "disable_info": "Vous pouvez également utiliser cette option pour désactiver la table des matières en définissant un nombre très élevé.", - "shortcut_info": "Vous pouvez configurer un raccourci clavier pour afficher/masquer le volet de droite (y compris la table des matières) dans Options -> Raccourcis (nom « toggleRightPane »)." + "shortcut_info": "Vous pouvez configurer un raccourci clavier pour afficher/masquer le volet de droite (y compris la table des matières) dans Options -> Raccourcis (nom « toggleRightPane »).", + "unit": "titres" }, "text_auto_read_only_size": { "title": "Taille automatique en lecture seule", "description": "La taille automatique des notes en lecture seule est la taille au-delà de laquelle les notes seront affichées en mode lecture seule (pour des raisons de performances).", - "label": "Taille automatique en lecture seule (notes de texte)" + "label": "Taille automatique en lecture seule (notes de texte)", + "unit": "caractères" }, "i18n": { "title": "Paramètres régionaux", "language": "Langue", "first-day-of-the-week": "Premier jour de la semaine", "sunday": "Dimanche", - "monday": "Lundi" + "monday": "Lundi", + "tuesday": "Mardi", + "wednesday": "Mercredi", + "thursday": "Jeudi", + "friday": "Vendredi", + "saturday": "Samedi", + "first-week-of-the-year": "Première semaine de l'année", + "first-week-contains-first-day": "La première semaine contient le premier jour de l'année", + "first-week-contains-first-thursday": "La première semaine contient le premier jeudi de l'année", + "first-week-has-minimum-days": "La première semaine a un nombre minimum de jours", + "min-days-in-first-week": "Nombre minimum de jours dans la première semaine", + "first-week-info": "La première semaine contient le premier jeudi de l'année et est basée sur la norme ISO 8601 .", + "first-week-warning": "La modification des options de la première semaine peut entraîner des doublons avec les notes de semaine existantes et les notes de semaine existantes ne seront pas mises à jour en conséquence.", + "formatting-locale": "Format de date et de nombre", + "formatting-locale-auto": "En fonction de la langue de l'application" }, "backup": { "automatic_backup": "Sauvegarde automatique", @@ -1284,7 +1303,9 @@ "delete_token": "Supprimer/désactiver ce token", "rename_token_title": "Renommer le jeton", "rename_token_message": "Veuillez saisir le nom du nouveau jeton", - "delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?" + "delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?", + "see_more": "Voir plus de détails dans le {{- link_to_wiki}} et le {{- link_to_openapi_spec}} ou le {{- link_to_swagger_ui }}.", + "swagger_ui": "Interface utilisateur ETAPI Swagger" }, "options_widget": { "options_status": "Statut des options", @@ -1688,7 +1709,12 @@ "minimum_input": "La valeur de temps saisie doit être d'au moins {{minimumSeconds}} secondes." }, "multi_factor_authentication": { - "oauth_user_email": "Courriel de l'utilisateur : " + "oauth_user_email": "Courriel de l'utilisateur : ", + "title": "Authentification multifacteur", + "description": "L'authentification multifacteur (MFA) renforce la sécurité de votre compte. Au lieu de simplement saisir un mot de passe pour vous connecter, le MFA vous demande de fournir une ou plusieurs preuves supplémentaires pour vérifier votre identité. Ainsi, même si quelqu'un obtient votre mot de passe, il ne peut accéder à votre compte sans cette deuxième information. C'est comme ajouter une serrure supplémentaire à votre porte, rendant l'effraction beaucoup plus difficile.

    Veuillez suivre les instructions ci-dessous pour activer le MFA. Si vous ne configurez pas correctement, la connexion se fera uniquement par mot de passe.", + "mfa_enabled": "Activer l'authentification multifacteur", + "mfa_method": "Méthode MFA", + "electron_disabled": "L'authentification multifacteur n'est actuellement pas prise en charge dans la version de bureau." }, "modal": { "close": "Fermer" @@ -1786,5 +1812,11 @@ "enable-backdrop-effects": "Activer les effets d'arrière plan pour les menus, popups et panneaux", "enable-smooth-scroll": "Active le défilement fluide", "app-restart-required": "(redémarrer l'application pour appliquer les changements)" + }, + "custom_date_time_format": { + "title": "Format de date/heure personnalisé", + "description": "Personnalisez le format de la date et de l'heure insérées via ou la barre d'outils. Consultez la documentation Day.js pour connaître les formats disponibles.", + "format_string": "Chaîne de format :", + "formatted_time": "Date/heure formatée :" } } From 45de9da893d381870aa27562ebcda522790d64a4 Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 21 Oct 2025 23:00:54 +0200 Subject: [PATCH 039/250] Translated using Weblate (French) Currently translated at 90.9% (352 of 387 strings) Translation: Trilium Notes/Server Translate-URL: https://hosted.weblate.org/projects/trilium/server/fr/ --- .../src/assets/translations/fr/server.json | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/server/src/assets/translations/fr/server.json b/apps/server/src/assets/translations/fr/server.json index 1b263d08c..88dc6a713 100644 --- a/apps/server/src/assets/translations/fr/server.json +++ b/apps/server/src/assets/translations/fr/server.json @@ -250,7 +250,13 @@ "other": "Autre", "advanced-title": "Avancé", "visible-launchers-title": "Raccourcis visibles", - "user-guide": "Guide de l'utilisateur" + "user-guide": "Guide de l'utilisateur", + "jump-to-note-title": "Aller à...", + "llm-chat-title": "Discuter avec Notes", + "multi-factor-authentication-title": "MFA", + "ai-llm-title": "AI/LLM", + "localization": "Langue et région", + "inbox-title": "Boîte de réception" }, "notes": { "new-note": "Nouvelle note", @@ -375,7 +381,14 @@ "zoom-in": "Zoomer", "reset-zoom-level": "Réinitilaliser le zoom", "copy-without-formatting": "Copier sans mise en forme", - "force-save-revision": "Forcer la sauvegarde de la révision" + "force-save-revision": "Forcer la sauvegarde de la révision", + "toggle-ribbon-tab-promoted-attributes": "Basculer les attributs promus de l'onglet du ruban", + "toggle-ribbon-tab-note-map": "Basculer l'onglet du ruban Note Map", + "toggle-ribbon-tab-note-info": "Basculer l'onglet du ruban Note Info", + "toggle-ribbon-tab-note-paths": "Basculer les chemins de notes de l'onglet du ruban", + "toggle-ribbon-tab-similar-notes": "Basculer l'onglet du ruban Notes similaires", + "toggle-note-hoisting": "Activer la focalisation sur la note", + "unhoist-note": "Désactiver la focalisation sur la note" }, "sql_init": { "db_not_initialized_desktop": "Base de données non initialisée, merci de suivre les instructions à l'écran.", @@ -383,5 +396,7 @@ }, "desktop": { "instance_already_running": "Une instance est déjà en cours d'execution, ouverture de cette instance à la place." - } + }, + "weekdayNumber": "Semaine {weekNumber}", + "quarterNumber": "Trimestre {quarterNumber}" } From c4a4995da0863ab15cdf909b2bfbb7ae2a9107fe Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 21 Oct 2025 22:55:55 +0200 Subject: [PATCH 040/250] Translated using Weblate (French) Currently translated at 71.9% (105 of 146 strings) Translation: Trilium Notes/Website Translate-URL: https://hosted.weblate.org/projects/trilium/website/fr/ --- .../public/translations/fr/translation.json | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/apps/website/public/translations/fr/translation.json b/apps/website/public/translations/fr/translation.json index 32b521fea..e8f10ab71 100644 --- a/apps/website/public/translations/fr/translation.json +++ b/apps/website/public/translations/fr/translation.json @@ -44,6 +44,98 @@ "code_title": "Notes de code" }, "faq": { - "database_question": "Où sont les données stockées?" + "database_question": "Où sont les données stockées?", + "database_answer": "Toutes vos notes seront stockées dans une base de données SQLite, dans un dossier d'application. Trilium utilise une base de données plutôt que des fichiers texte pour des raisons de performances, et certaines fonctionnalités seraient beaucoup plus complexes à implémenter, comme les clones (même note à plusieurs endroits de l'arborescence). Pour trouver le dossier d'application, accédez simplement à la fenêtre « À propos ».", + "mobile_answer": "Il n'existe actuellement aucune application mobile officielle. Cependant, si vous disposez d'une instance serveur, vous pouvez y accéder via un navigateur web et même l'installer en tant que PWA. Pour Android, il existe une application non officielle appelée \"TriliumDroid\", qui fonctionne même hors ligne (comme un client de bureau).", + "mobile_question": "Y a-t-il une application mobile ?", + "title": "Foire aux questions", + "server_question": "Ai-je besoin d'un serveur pour utiliser Trilium ?", + "server_answer": "Non, le serveur permet l'accès via un navigateur web et gère la synchronisation si vous possédez plusieurs appareils. Pour commencer, il suffit de télécharger l'application de bureau et de l'utiliser.", + "scaling_question": "Dans quelle mesure l'application s'adapte-t-elle à une grande quantité de notes ?", + "scaling_answer": "Selon l'utilisation, l'application devrait pouvoir gérer au moins 100 000 notes sans problème. Notez que la synchronisation peut parfois échouer lors du téléchargement de nombreux fichiers volumineux (1 Go par fichier), car Trilium est davantage conçu comme une base de connaissances que comme un espace de stockage de fichiers (comme NextCloud, par exemple).", + "network_share_question": "Puis-je partager ma base de données sur un lecteur réseau ?", + "network_share_answer": "Non, il est généralement déconseillé de partager une base de données SQLite sur un lecteur réseau. Bien que cela puisse parfois fonctionner, il existe un risque de corruption de la base de données en raison de verrous de fichiers imparfaits sur le réseau.", + "security_question": "Comment mes données sont-elles protégées?", + "security_answer": "Par défaut, les notes ne sont pas chiffrées et peuvent être consultées directement depuis la base de données. Une fois chiffrée, une note l'est avec le protocole AES-128-CBC." + }, + "final_cta": { + "title": "Prêt à commencer avec Trilium Notes ?", + "description": "Créez votre base de connaissances personnelle avec des fonctionnalités puissantes et une totale confidentialité.", + "get_started": "Commencer" + }, + "components": { + "link_learn_more": "En savoir plus..." + }, + "support_us": { + "financial_donations_title": "Dons financiers", + "financial_donations_description": "Trilium est développé et maintenu grâce à des centaines d'heures de travail. Votre soutien permet de maintenir son open-source, d'améliorer ses fonctionnalités et de couvrir des coûts tels que l'hébergement.", + "financial_donations_cta": "Envisagez de soutenir le développeur principal (eliandoran) de l'application via :", + "github_sponsors": "Sponsors GitHub", + "paypal": "PayPal", + "buy_me_a_coffee": "Offrez-moi un café" + }, + "contribute": { + "title": "Autres façons de contribuer", + "way_translate": "Traduisez l'application dans votre langue maternelle via Weblate.", + "way_community": "Interagissez avec la communauté sur GitHub Discussions ou sur Matrix.", + "way_reports": "Signalez les bugs via GitHub issues.", + "way_document": "Améliorez la documentation en nous informant des lacunes dans celle-ci ou en contribuant à des guides, des FAQ ou des tutoriels.", + "way_market": "Passez le mot : partagez Trilium Notes avec vos amis, sur des blogs et sur les réseaux sociaux." + }, + "404": { + "title": "404 : introuvable", + "description": "La page que vous cherchiez est introuvable. Elle a peut-être été supprimée ou l'URL est incorrecte." + }, + "download_helper_desktop_windows": { + "title_x64": "Windows 64-bit", + "title_arm64": "Windows sur ARM", + "description_x64": "Compatible avec les appareils Intel ou AMD exécutant Windows 10 et 11.", + "description_arm64": "Compatible avec les appareils ARM (par exemple avec Qualcomm Snapdragon).", + "quick_start": "Pour installer via Winget :", + "download_exe": "télécharger l'installeur (.exe)", + "download_zip": "Portable (.zip)", + "download_scoop": "Scoop" + }, + "download_helper_desktop_linux": { + "title_x64": "Linux 64-bit", + "title_arm64": "Linux sur ARM", + "description_x64": "Pour la plupart des distributions Linux, compatible avec l'architecture x86_64.", + "description_arm64": "Pour les distributions Linux basées sur ARM, compatible avec l'architecture aarch64.", + "quick_start": "Sélectionnez un format de package approprié, en fonction de votre distribution :", + "download_deb": ".deb", + "download_rpm": ".rpm", + "download_flatpak": ".flatpak", + "download_zip": "Portable (.zip)", + "download_nixpkgs": "nixpkgs", + "download_aur": "AUR" + }, + "download_helper_desktop_macos": { + "title_x64": "macOS pour Intel", + "title_arm64": "macOS pour Apple Silicon", + "description_x64": "Pour les Mac basés sur Intel exécutant macOS Big Sur ou une version ultérieure.", + "description_arm64": "Pour les Mac Apple Silicon tels que ceux équipés de puces M1 et M2.", + "quick_start": "Pour installer via Homebrew :", + "download_dmg": "Télécharger le programme d'installation (.dmg)", + "download_homebrew_cask": "Homebrew Cask", + "download_zip": "Portable (.zip)" + }, + "download_helper_server_docker": { + "title": "Auto-hébergé avec Docker", + "description": "Déployez facilement sur Windows, Linux ou macOS à l'aide d'un conteneur Docker.", + "download_dockerhub": "Docker Hub", + "download_ghcr": "ghcr.io" + }, + "download_helper_server_linux": { + "title": "Auto-hébergé sur Linux", + "description": "Déployez Trilium Notes sur votre propre serveur ou VPS, compatible avec la plupart des distributions.", + "download_tar_x64": "x64 (.tar.xz)", + "download_tar_arm64": "ARM (.tar.xz)", + "download_nixos": "Module NixOS" + }, + "download_helper_server_hosted": { + "title": "Hébergement payant", + "description": "Notes Trilium hébergées sur PikaPods, un service payant pour un accès et une gestion simplifiés. Non affilié directement à l'équipe Trilium.", + "download_pikapod": "Installé sur PikaPods", + "download_triliumcc": "Voir également trilium.cc" } } From 14db789b7fff487a5b81ea8089e5122acac1b312 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 22 Oct 2025 07:53:45 +0300 Subject: [PATCH 041/250] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- docs/README-fr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README-fr.md b/docs/README-fr.md index 9d138830a..27f384566 100644 --- a/docs/README-fr.md +++ b/docs/README-fr.md @@ -207,7 +207,7 @@ Actuellement, seules les dernières versions de Chrome & Firefox sont supportée ### Mobile Pour utiliser TriliumNext sur un appareil mobile, vous pouvez utiliser un -navigateur Web afin d' accéder à l'interface d'une installation serveur (voir +navigateur Web afin d'accéder à l'interface d'une installation serveur (voir ci-dessous). Pour plus d’informations sur le support de l’application mobile, consultez le From 651e158e3a9b5f52ee272705a5517250aa674c62 Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 22 Oct 2025 00:15:10 +0200 Subject: [PATCH 042/250] Translated using Weblate (French) Currently translated at 88.8% (1440 of 1621 strings) Translation: Trilium Notes/Client Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/ --- .../src/translations/fr/translation.json | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json index 9d97cd345..22e2363a4 100644 --- a/apps/client/src/translations/fr/translation.json +++ b/apps/client/src/translations/fr/translation.json @@ -1368,7 +1368,8 @@ "test_title": "Test de synchronisation", "test_description": "Testera la connexion et la prise de contact avec le serveur de synchronisation. Si le serveur de synchronisation n'est pas initialisé, cela le configurera pour qu'il se synchronise avec le document local.", "test_button": "Tester la synchronisation", - "handshake_failed": "Échec de la négociation avec le serveur de synchronisation, erreur : {{message}}" + "handshake_failed": "Échec de la négociation avec le serveur de synchronisation, erreur : {{message}}", + "timeout_unit": "millisecondes" }, "api_log": { "close": "Fermer" @@ -1428,7 +1429,10 @@ "import-into-note": "Importer dans la note", "apply-bulk-actions": "Appliquer des Actions groupées", "converted-to-attachments": "Les notes {{count}} ont été converties en pièces jointes.", - "convert-to-attachment-confirm": "Êtes-vous sûr de vouloir convertir les notes sélectionnées en pièces jointes de leurs notes parentes ?" + "convert-to-attachment-confirm": "Êtes-vous sûr de vouloir convertir les notes sélectionnées en pièces jointes de leurs notes parentes ?", + "archive": "Archive", + "unarchive": "Désarchiver", + "open-in-popup": "Modification rapide" }, "shared_info": { "shared_publicly": "Cette note est partagée publiquement sur {{- link}}", @@ -1454,7 +1458,9 @@ "confirm-change": "Il n'est pas recommandé de modifier le type de note lorsque son contenu n'est pas vide. Voulez-vous continuer ?", "geo-map": "Carte géo", "beta-feature": "Beta", - "task-list": "Liste de tâches" + "task-list": "Liste de tâches", + "book": "Collection", + "ai-chat": "Chat IA" }, "protect_note": { "toggle-on": "Protéger la note", @@ -1714,7 +1720,32 @@ "description": "L'authentification multifacteur (MFA) renforce la sécurité de votre compte. Au lieu de simplement saisir un mot de passe pour vous connecter, le MFA vous demande de fournir une ou plusieurs preuves supplémentaires pour vérifier votre identité. Ainsi, même si quelqu'un obtient votre mot de passe, il ne peut accéder à votre compte sans cette deuxième information. C'est comme ajouter une serrure supplémentaire à votre porte, rendant l'effraction beaucoup plus difficile.

    Veuillez suivre les instructions ci-dessous pour activer le MFA. Si vous ne configurez pas correctement, la connexion se fera uniquement par mot de passe.", "mfa_enabled": "Activer l'authentification multifacteur", "mfa_method": "Méthode MFA", - "electron_disabled": "L'authentification multifacteur n'est actuellement pas prise en charge dans la version de bureau." + "electron_disabled": "L'authentification multifacteur n'est actuellement pas prise en charge dans la version de bureau.", + "totp_title": "Mot de passe à usage unique basé sur le temps (TOTP)", + "totp_description": "Le TOTP (Time-Based One-Time Password) est une fonctionnalité de sécurité qui génère un code unique et temporaire, modifié toutes les 30 secondes. Vous utilisez ce code, associé à votre mot de passe, pour vous connecter à votre compte, ce qui rend l'accès à celui-ci beaucoup plus difficile.", + "totp_secret_title": "Générer un secret TOTP", + "totp_secret_generate": "Générer un secret TOTP", + "totp_secret_regenerate": "Re-générer un secret TOTP", + "no_totp_secret_warning": "Pour activer TOTP, vous devez d’abord générer un secret TOTP.", + "totp_secret_description_warning": "Après avoir généré un nouveau secret TOTP, vous devrez vous reconnecter avec le nouveau secret TOTP.", + "totp_secret_generated": "Secret TOTP généré", + "totp_secret_warning": "Veuillez conserver le secret généré dans un endroit sûr. Il ne sera plus affiché.", + "totp_secret_regenerate_confirm": "Voulez-vous vraiment régénérer le secret TOTP ? Cela invalidera le secret TOTP précédent et tous les codes de récupération existants.", + "recovery_keys_title": "Clés de récupération d'authentification unique", + "recovery_keys_description": "Les clés de récupération d'authentification unique sont utilisées pour vous connecter même si vous ne pouvez pas accéder à vos codes d'authentification.", + "recovery_keys_description_warning": "Les clés de récupération ne seront plus affichées après avoir quitté la page, conservez-les dans un endroit sûr et sécurisé.
    Une fois qu'une clé de récupération a été utilisée, elle devient inutilisable.", + "recovery_keys_error": "Erreur lors de la génération des codes de récupération", + "recovery_keys_no_key_set": "Aucun code de récupération défini", + "recovery_keys_generate": "Générer des codes de récupération", + "recovery_keys_regenerate": "Re-générer des codes de récupération", + "recovery_keys_used": "Utilisé : {{date}}", + "recovery_keys_unused": "Le code de récupération {{index}} n'est pas utilisé", + "oauth_title": "OAuth/OpenID", + "oauth_description": "OpenID est un moyen standardisé de vous connecter à des sites web avec un compte d'un autre service, comme Google, afin de vérifier votre identité. L'émetteur par défaut est Google, mais vous pouvez le modifier pour n'importe quel autre fournisseur OpenID. Consultez ici pour plus d'informations. Suivez ces instructions pour configurer un service OpenID via Google.", + "oauth_description_warning": "Pour activer OAuth/OpenID, vous devez définir l'URL de base, l'ID client et le secret client OAuth/OpenID dans le fichier config.ini, puis redémarrer l'application. Pour les définir à partir des variables d'environnement, définissez TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID et TRILIUM_OAUTH_CLIENT_SECRET.", + "oauth_missing_vars": "Paramètres manquants : {{-variables}}", + "oauth_user_account": "Compte utilisateur: ", + "oauth_user_not_logged_in": "Pas connecté !" }, "modal": { "close": "Fermer" From 9a0b4f67ed3a7d99756b730da5c8106784d42da4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 22 Oct 2025 06:54:52 +0200 Subject: [PATCH 043/250] Update translation files Updated by "Cleanup translation files" add-on in Weblate. Translation: Trilium Notes/README Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ --- docs/README-fr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README-fr.md b/docs/README-fr.md index 27f384566..9d138830a 100644 --- a/docs/README-fr.md +++ b/docs/README-fr.md @@ -207,7 +207,7 @@ Actuellement, seules les dernières versions de Chrome & Firefox sont supportée ### Mobile Pour utiliser TriliumNext sur un appareil mobile, vous pouvez utiliser un -navigateur Web afin d'accéder à l'interface d'une installation serveur (voir +navigateur Web afin d' accéder à l'interface d'une installation serveur (voir ci-dessous). Pour plus d’informations sur le support de l’application mobile, consultez le From 93f145a20fd7690b27ce5c53da02c3567e4b04e0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 22 Oct 2025 17:44:19 +0300 Subject: [PATCH 044/250] fix(forge): missing flatpak permissions (closes #7454) --- apps/desktop/electron-forge/forge.config.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/desktop/electron-forge/forge.config.ts b/apps/desktop/electron-forge/forge.config.ts index 0b78b3e1f..0c0531c8e 100644 --- a/apps/desktop/electron-forge/forge.config.ts +++ b/apps/desktop/electron-forge/forge.config.ts @@ -70,7 +70,6 @@ const config: ForgeConfig = { ] }, rebuildConfig: { - force: true, extraModules: [ "better-sqlite3" ] }, makers: [ @@ -91,8 +90,20 @@ const config: ForgeConfig = { baseVersion: "24.08", baseFlatpakref: "https://flathub.org/repo/flathub.flatpakrepo", finishArgs: [ + // Wayland/X11 Rendering "--socket=fallback-x11", - "--socket=wayland" + "--socket=wayland", + "--share=ipc", + // Open GL + "--device=dri", + // Audio output + "--socket=pulseaudio", + // Read/write home directory access + "--filesystem=home", + // Allow communication with network + "--share=network", + // System notifications with libnotify + "--talk-name=org.freedesktop.Notifications", ], modules: [ { From cb3f941760930625a00a8d79e5c3bc814b28858f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 22 Oct 2025 17:45:31 +0300 Subject: [PATCH 045/250] chore(forge): add script to make flatpak --- apps/desktop/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ce08cfaf6..581c82df4 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -16,6 +16,7 @@ "build": "tsx scripts/build.ts", "start-prod": "pnpm build && cross-env TRILIUM_DATA_DIR=data TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist", "electron-forge:make": "pnpm build && cross-env electron-forge make dist", + "electron-forge:make-flatpak": "cross-env electron-forge make dist --targets=@electron-forge/maker-flatpak", "electron-forge:package": "pnpm build && electron-forge package dist", "electron-forge:start": "pnpm build && electron-forge start dist", "e2e": "pnpm build && cross-env TRILIUM_INTEGRATION_TEST=memory-no-store TRILIUM_PORT=8082 TRILIUM_DATA_DIR=data-e2e ELECTRON_IS_DEV=0 playwright test" From f02af893bb707834d6aecffeb4995ea08daffdbd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 22 Oct 2025 18:03:52 +0300 Subject: [PATCH 046/250] fix(forge): wrong exec in flatpak desktop template (closes #5516) --- apps/desktop/electron-forge/forge.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/electron-forge/forge.config.ts b/apps/desktop/electron-forge/forge.config.ts index 0c0531c8e..47a91b4ed 100644 --- a/apps/desktop/electron-forge/forge.config.ts +++ b/apps/desktop/electron-forge/forge.config.ts @@ -84,6 +84,7 @@ const config: ForgeConfig = { config: { options: { ...baseLinuxMakerConfigOptions, + desktopTemplate: undefined, id: "com.triliumnext.notes", runtimeVersion: "24.08", base: "org.electronjs.Electron2.BaseApp", From 0b808b8db32a30b269fe570fe3463fcaeb29b20d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 22 Oct 2025 18:05:38 +0300 Subject: [PATCH 047/250] fix(forge): missing icon in flatpak build --- apps/desktop/electron-forge/forge.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron-forge/forge.config.ts b/apps/desktop/electron-forge/forge.config.ts index 47a91b4ed..1dc5310ff 100644 --- a/apps/desktop/electron-forge/forge.config.ts +++ b/apps/desktop/electron-forge/forge.config.ts @@ -84,7 +84,10 @@ const config: ForgeConfig = { config: { options: { ...baseLinuxMakerConfigOptions, - desktopTemplate: undefined, + desktopTemplate: undefined, // otherwise it would put in the wrong exec + icon: { + "128x128": path.join(APP_ICON_PATH, "png/128x128.png"), + }, id: "com.triliumnext.notes", runtimeVersion: "24.08", base: "org.electronjs.Electron2.BaseApp", From 2cb3b877d148b212e6e1d669d209b5e5edfd2199 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 22 Oct 2025 18:24:34 +0300 Subject: [PATCH 048/250] chore(forge): fixup electron-forge:make scripts --- apps/desktop/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 581c82df4..1df37f089 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -15,8 +15,8 @@ "start-no-dir": "cross-env TRILIUM_PORT=37743 tsx ../../scripts/electron-start.mts src/main.ts", "build": "tsx scripts/build.ts", "start-prod": "pnpm build && cross-env TRILIUM_DATA_DIR=data TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist", - "electron-forge:make": "pnpm build && cross-env electron-forge make dist", - "electron-forge:make-flatpak": "cross-env electron-forge make dist --targets=@electron-forge/maker-flatpak", + "electron-forge:make": "pnpm build && electron-forge make dist", + "electron-forge:make-flatpak": "pnpm build && electron-forge make dist --targets=@electron-forge/maker-flatpak", "electron-forge:package": "pnpm build && electron-forge package dist", "electron-forge:start": "pnpm build && electron-forge start dist", "e2e": "pnpm build && cross-env TRILIUM_INTEGRATION_TEST=memory-no-store TRILIUM_PORT=8082 TRILIUM_DATA_DIR=data-e2e ELECTRON_IS_DEV=0 playwright test" From f6b86d725c6ccd6da550b6e129ee880b5af9c19d Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 22 Oct 2025 17:27:50 +0200 Subject: [PATCH 049/250] Update translation files Updated by "Cleanup translation files" add-on in Weblate. Translation: Trilium Notes/README Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ --- docs/README-fr.md | 179 ++++++++++++++++++++++++---------------------- docs/README-it.md | 8 +-- 2 files changed, 96 insertions(+), 91 deletions(-) diff --git a/docs/README-fr.md b/docs/README-fr.md index 9d138830a..962db0f81 100644 --- a/docs/README-fr.md +++ b/docs/README-fr.md @@ -11,15 +11,14 @@ # Trilium Notes -![Sponsors GitHub](https://img.shields.io/github/sponsors/eliandoran) -![Contributeurs LiberaPay](https://img.shields.io/liberapay/patrons/ElianDoran)\ -![Téléchargements -Docker](https://img.shields.io/docker/pulls/triliumnext/trilium) -![Téléchargements GitHub (toutes les ressources, toutes les -versions)](https://img.shields.io/github/downloads/triliumnext/trilium/total)\ +![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran) +![LiberaPay patrons](https://img.shields.io/liberapay/patrons/ElianDoran)\ +![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/trilium) +![GitHub Downloads (all assets, all +releases)](https://img.shields.io/github/downloads/triliumnext/trilium/total)\ [![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) -[![État de la -traduction](https://hosted.weblate.org/widget/trilium/svg-badge.svg)](https://hosted.weblate.org/engage/trilium/) +[![Translation +status](https://hosted.weblate.org/widget/trilium/svg-badge.svg)](https://hosted.weblate.org/engage/trilium/) [Anglais](./README.md) | [Chinois (simplifié)](./docs/README-ZH_CN.md) | [Chinois (Traditionnel)](./docs/README-ZH_TW.md) | [Russe](./docs/README-ru.md) @@ -31,7 +30,8 @@ prise de notes hiérarchique, conçue pour créer et gérer de vastes bases de connaissances personnelles. Voir [les captures d'écran] -(https://triliumnext.github.io/Docs/Wiki/screenshot-tour) pour un aperçu rapide: +(https://triliumnext.github.io/Docs/Wiki/screenshot-tour) pour un aperçu rapide +: Trilium Screenshot @@ -47,8 +47,8 @@ Voir [les captures d'écran] **Visitez notre documentation complète sur [docs.triliumnotes.org](https://docs.triliumnotes.org/)** -Notre documentation est disponible sous plusieurs formats: -- ** Documentation en ligne**: Parcourez la documentation complète sur +Notre documentation est disponible sous plusieurs formats : +- **Documentation en ligne**: Parcourez la documentation complète sur [docs.triliumnotes.org](https://docs.triliumnotes.org/) - **Aide intégrée**: Appuyez sur `F1` dans Trilium pour accéder à la même documentation directement dans l'application @@ -104,72 +104,77 @@ Notre documentation est disponible sous plusieurs formats: notes sur Internet * [Cryptage de note](https://triliumnext.github.io/Docs/Wiki/protected-notes) fort avec granularité par note -* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type - "canvas") -* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and - [link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing - notes and their relations -* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/) -* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with - location pins and GPX tracks -* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced - showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases) -* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation -* Scales well in both usability and performance upwards of 100 000 notes -* Touch optimized [mobile - frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for - smartphones and tablets -* Built-in [dark theme](https://triliumnext.github.io/Docs/Wiki/themes), support - for user themes -* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and - [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown) -* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy - saving of web content -* Customizable UI (sidebar buttons, user-defined widgets, ...) -* [Metrics](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), along - with a [Grafana - Dashboard](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json) +* Diagrammes d'esquisse, basés sur [Excalidraw](https://excalidraw.com/) (type + de note "canvas")) +* [Cartes de relations](https://triliumnext.github.io/Docs/Wiki/relation-map) et + [cartes de liens](https://triliumnext.github.io/Docs/Wiki/link-map) pour + visualiser les notes et leurs relations +* Cartes mentales, basées sur [Mind Elixir] (https://docs.mind-elixir.com/) +* [Cartes + géographiques](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) + avec repères de localisation et pistes GPX +* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - voir [Vitrines + avancées](https://triliumnext.github.io/Docs/Wiki/advanced-showcases) +* [API REST](https://triliumnext.github.io/Docs/Wiki/etapi) pour + l'automatisation +* Optimisé en termes d’ergonomie et de performances, même au-delà de 100 000 + notes +* [Interface mobile](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) + optimisée pour le tactile sur smartphones et tablettes +* [Thème sombre](https://triliumnext.github.io/Docs/Wiki/themes) intégré, prise + en charge des thèmes utilisateur +* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) et + [Importation et exportation + Markdown](https://triliumnext.github.io/Docs/Wiki/markdown) +* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) pour une + sauvegarde facile du contenu web +* Interface utilisateur personnalisable (boutons de la barre latérale, widgets + définis par l'utilisateur, ...) +* [Métriques](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), + ainsi que [Tableau de bord + Grafana](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json) -✨ Check out the following third-party resources/communities for more TriliumNext -related goodies: +✨ Consultez les ressources/communautés tierces suivantes pour plus de +fonctionnalités liées à TriliumNext : -- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party - themes, scripts, plugins and more. -- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more. +- [awesome-trilium](https://github.com/Nriver/awesome-trilium) pour des thèmes, + scripts, plugins et plus encore tiers. +- [TriliumRocks!](https://trilium.rocks/) pour des tutoriels, des guides et bien + plus encore. -## ❓Why TriliumNext? +## ❓Pourquoi TriliumNext ? -The original Trilium developer ([Zadam](https://github.com/zadam)) has -graciously given the Trilium repository to the community project which resides -at https://github.com/TriliumNext +Le développeur original de Trilium ([Zadam](https://github.com/zadam)) a +gracieusement donné le référentiel Trilium au projet communautaire hébergé sur +https://github.com/TriliumNext -### ⬆️Migrating from Zadam/Trilium? +### ⬆️Migration depuis Zadam/Trilium ? -There are no special migration steps to migrate from a zadam/Trilium instance to -a TriliumNext/Trilium instance. Simply [install -TriliumNext/Trilium](#-installation) as usual and it will use your existing -database. +Il n'y a aucune étape de migration spécifique pour migrer d'une instance +zadam/Trilium vers une instance TriliumNext/Trilium. Installez simplement +TriliumNext/Trilium comme d'habitude et votre base de données existante sera +utilisée. -Versions up to and including -[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are -compatible with the latest zadam/trilium version of -[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later -versions of TriliumNext/Trilium have their sync versions incremented which -prevents direct migration. +Les versions jusqu'à +[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) incluses +sont compatibles avec la dernière version de zadam/trilium +[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Les versions +ultérieures de TriliumNext/Trilium voient leurs versions synchronisées +incrémentées, ce qui empêche toute migration directe. -## 💬 Discuss with us +## 💬 Discutez avec nous -Feel free to join our official conversations. We would love to hear what -features, suggestions, or issues you may have! +N'hésitez pas à participer à nos discussions officielles. Nous serions ravis de +connaître vos idées, suggestions ou problèmes ! -- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous - discussions.) - - The `General` Matrix room is also bridged to +- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (Pour les discussions + synchrones.) + - L'espace Matrix `Général` est également reliée à [XMPP](xmpp:discuss@trilium.thisgreat.party?join) -- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For - asynchronous discussions.) -- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug - reports and feature requests.) +- [Discussions Github](https://github.com/TriliumNext/Trilium/discussions) (Pour + les discussions asynchrones.) +- [Problèmes Github](https://github.com/TriliumNext/Trilium/issues) (Pour les + rapports de bogues et les demandes de fonctionnalités.) ## 🏗 Installation @@ -184,7 +189,7 @@ décompressez le package et exécutez l'exécutable `trilium`. Si votre distribution est répertoriée dans le tableau ci-dessous, utilisez le package de votre distribution. -[![État du +[![Statut du Packaging](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions) Vous pouvez également télécharger la version binaire pour votre plateforme à @@ -231,20 +236,20 @@ serveur](https://triliumnext.github.io/Docs/Wiki/server-installation). ## 💻 Contribuer -### Translations +### Traductions -If you are a native speaker, help us translate Trilium by heading over to our -[Weblate page](https://hosted.weblate.org/engage/trilium/). +Si vous êtes un locuteur natif, aidez-nous à traduire Trilium en vous rendant +sur notre [page Weblate](https://hosted.weblate.org/engage/trilium/). -Here's the language coverage we have so far: +Voici la couverture linguistique dont nous disposons jusqu'à présent : -[![Translation -status](https://hosted.weblate.org/widget/trilium/multi-auto.svg)](https://hosted.weblate.org/engage/trilium/) +[ ![Statut de la +traduction](https://hosted.weblate.org/widget/trilium/multi-auto.svg)](https://hosted.weblate.org/engage/trilium/) ### Code -Download the repository, install dependencies using `pnpm` and then run the -server (available at http://localhost:8080): +Téléchargez le référentiel, installez les dépendances à l'aide de `pnpm` puis +exécutez le serveur (disponible sur http://localhost:8080) : ```shell git clone https://github.com/TriliumNext/Trilium.git cd Trilium @@ -254,8 +259,8 @@ pnpm run server:start ### Documentation -Download the repository, install dependencies using `pnpm` and then run the -environment required to edit the documentation: +Téléchargez le référentiel, installez les dépendances à l'aide de `pnpm`, puis +exécutez l'environnement requis pour modifier la documentation : ```shell git clone https://github.com/TriliumNext/Trilium.git cd Trilium @@ -263,9 +268,9 @@ pnpm install pnpm edit-docs:edit-docs ``` -### Building the Executable -Download the repository, install dependencies using `pnpm` and then build the -desktop app for Windows: +### Générer l'exécutable +Téléchargez le référentiel, installez les dépendances à l'aide de `pnpm`, puis +créez l'application de bureau pour Windows : ```shell git clone https://github.com/TriliumNext/Trilium.git cd Trilium @@ -273,15 +278,15 @@ pnpm install pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32 ``` -For more details, see the [development -docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide). +Pour plus de détails, consultez la [documentation de +développement](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide). -### Developer Documentation +### Documentation du développeur -Please view the [documentation -guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) -for details. If you have more questions, feel free to reach out via the links -described in the "Discuss with us" section above. +Veuillez consulter le [guide de +documentation](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) +pour plus de détails. Pour toute question, n'hésitez pas à nous contacter via +les liens décrits dans la section "Discuter avec nous" ci-dessus. ## 👏 Dédicaces diff --git a/docs/README-it.md b/docs/README-it.md index 3e375980f..eb6a365e6 100644 --- a/docs/README-it.md +++ b/docs/README-it.md @@ -34,12 +34,12 @@ una panoramica veloce: Trilium Screenshot -## ⏬ Download +## ⏬ Scarica - [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) – - stable version, recommended for most users. + versione stabile, consigliata per la maggior parte degli utenti. - [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly) – - unstable development version, updated daily with the latest features and - fixes. + versione di sviluppo instabile, aggiornata quotidianamente con le ultime + funzionalità e correzioni. ## 📚 Documentazione From 968b595aec393d71d4b25a0f78987f9860e8cd88 Mon Sep 17 00:00:00 2001 From: Jon Fuller Date: Wed, 22 Oct 2025 11:20:14 -0700 Subject: [PATCH 050/250] fix(scheduler): change session expiration check interval to 30 seconds from 1ms Increase interval for checking protected session expiration from 1ms to 30s. --- apps/server/src/services/scheduler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/services/scheduler.ts b/apps/server/src/services/scheduler.ts index e58ef07b4..f44e3aa43 100644 --- a/apps/server/src/services/scheduler.ts +++ b/apps/server/src/services/scheduler.ts @@ -66,7 +66,7 @@ sqlInit.dbReady.then(() => { ); } - setInterval(() => checkProtectedSessionExpiration(), 1); + setInterval(() => checkProtectedSessionExpiration(), 30000); }); function checkProtectedSessionExpiration() { From 7911973a83d929bc3cb5170b93b22fc90c15b140 Mon Sep 17 00:00:00 2001 From: Adorian Doran Date: Wed, 22 Oct 2025 21:52:08 +0300 Subject: [PATCH 051/250] style: fix broken colored links inside read-only notes --- apps/client/src/stylesheets/theme-dark.css | 2 +- apps/client/src/stylesheets/theme-light.css | 2 +- apps/client/src/stylesheets/theme-next-dark.css | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/src/stylesheets/theme-dark.css b/apps/client/src/stylesheets/theme-dark.css index 2d08163d6..3b434d529 100644 --- a/apps/client/src/stylesheets/theme-dark.css +++ b/apps/client/src/stylesheets/theme-dark.css @@ -86,7 +86,7 @@ body ::-webkit-calendar-picker-indicator { --custom-color: var(--dark-theme-custom-color); } -.reference-link, +:root .reference-link, .ck-content a.reference-link > span { color: var(--dark-theme-custom-color, inherit); } diff --git a/apps/client/src/stylesheets/theme-light.css b/apps/client/src/stylesheets/theme-light.css index 3bf49341c..2bdb06a1c 100644 --- a/apps/client/src/stylesheets/theme-light.css +++ b/apps/client/src/stylesheets/theme-light.css @@ -86,7 +86,7 @@ html { --custom-color: var(--light-theme-custom-color); } -.reference-link, +:root .reference-link, .ck-content a.reference-link > span { color: var(--light-theme-custom-color, inherit); } \ No newline at end of file diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index 5b012996e..8df2e2e0a 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -277,7 +277,7 @@ --custom-bg-color: hsl(var(--custom-color-hue), 20%, 33%, 0.4); } -.reference-link, +:root .reference-link, .ck-content a.reference-link > span { color: var(--dark-theme-custom-color, inherit); } From f5038a08e5a060b3b4d1e346f37025c98528fa43 Mon Sep 17 00:00:00 2001 From: Adorian Doran Date: Wed, 22 Oct 2025 22:05:16 +0300 Subject: [PATCH 052/250] style/board/board items: make the note custom color be applied again as the text color --- apps/client/src/stylesheets/theme-dark.css | 3 ++- apps/client/src/stylesheets/theme-light.css | 3 ++- apps/client/src/stylesheets/theme-next-dark.css | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/client/src/stylesheets/theme-dark.css b/apps/client/src/stylesheets/theme-dark.css index 3b434d529..fb2abb45d 100644 --- a/apps/client/src/stylesheets/theme-dark.css +++ b/apps/client/src/stylesheets/theme-dark.css @@ -87,7 +87,8 @@ body ::-webkit-calendar-picker-indicator { } :root .reference-link, -.ck-content a.reference-link > span { +.ck-content a.reference-link > span, +.board-note { color: var(--dark-theme-custom-color, inherit); } diff --git a/apps/client/src/stylesheets/theme-light.css b/apps/client/src/stylesheets/theme-light.css index 2bdb06a1c..c0b7c271f 100644 --- a/apps/client/src/stylesheets/theme-light.css +++ b/apps/client/src/stylesheets/theme-light.css @@ -87,6 +87,7 @@ html { } :root .reference-link, -.ck-content a.reference-link > span { +.ck-content a.reference-link > span, +.board-note { color: var(--light-theme-custom-color, inherit); } \ No newline at end of file diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index 8df2e2e0a..71cb17924 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -278,7 +278,8 @@ } :root .reference-link, -.ck-content a.reference-link > span { +.ck-content a.reference-link > span, +.board-note { color: var(--dark-theme-custom-color, inherit); } From 6eccaac4bb6ea5e278a99759acfef25f5b98cbf3 Mon Sep 17 00:00:00 2001 From: Adorian Doran Date: Wed, 22 Oct 2025 22:12:58 +0300 Subject: [PATCH 053/250] style: fix broken colored links inside read-only notes (hover color) --- apps/client/src/stylesheets/theme-dark.css | 1 + apps/client/src/stylesheets/theme-light.css | 1 + apps/client/src/stylesheets/theme-next-dark.css | 1 + 3 files changed, 3 insertions(+) diff --git a/apps/client/src/stylesheets/theme-dark.css b/apps/client/src/stylesheets/theme-dark.css index fb2abb45d..a356d32fd 100644 --- a/apps/client/src/stylesheets/theme-dark.css +++ b/apps/client/src/stylesheets/theme-dark.css @@ -87,6 +87,7 @@ body ::-webkit-calendar-picker-indicator { } :root .reference-link, +:root .reference-link:hover, .ck-content a.reference-link > span, .board-note { color: var(--dark-theme-custom-color, inherit); diff --git a/apps/client/src/stylesheets/theme-light.css b/apps/client/src/stylesheets/theme-light.css index c0b7c271f..872e7431f 100644 --- a/apps/client/src/stylesheets/theme-light.css +++ b/apps/client/src/stylesheets/theme-light.css @@ -87,6 +87,7 @@ html { } :root .reference-link, +:root .reference-link:hover, .ck-content a.reference-link > span, .board-note { color: var(--light-theme-custom-color, inherit); diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index 71cb17924..98fe11a4b 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -278,6 +278,7 @@ } :root .reference-link, +:root .reference-link:hover, .ck-content a.reference-link > span, .board-note { color: var(--dark-theme-custom-color, inherit); From 5ff07820d3709d1f869a96aa37688ff55a47f284 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 22 Oct 2025 22:37:46 +0300 Subject: [PATCH 054/250] chore(release): prepare for 0.99.3 --- apps/client/package.json | 2 +- apps/desktop/package.json | 2 +- apps/server/package.json | 2 +- docs/Release Notes/!!!meta.json | 110 +++++++++++------- .../Release Notes/Release Template.md | 3 + docs/Release Notes/Release Notes/v0.99.3.md | 36 ++++++ package.json | 2 +- packages/commons/package.json | 2 +- 8 files changed, 112 insertions(+), 47 deletions(-) create mode 100644 docs/Release Notes/Release Notes/v0.99.3.md diff --git a/apps/client/package.json b/apps/client/package.json index c2490ebfd..71957e495 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/client", - "version": "0.99.2", + "version": "0.99.3", "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", "private": true, "license": "AGPL-3.0-only", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1df37f089..352e43a1c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/desktop", - "version": "0.99.2", + "version": "0.99.3", "description": "Build your personal knowledge base with Trilium Notes", "private": true, "main": "src/main.ts", diff --git a/apps/server/package.json b/apps/server/package.json index ec76897aa..5cc30b1c9 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/server", - "version": "0.99.2", + "version": "0.99.3", "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, "main": "./src/main.ts", diff --git a/docs/Release Notes/!!!meta.json b/docs/Release Notes/!!!meta.json index a33433340..57e84dc42 100644 --- a/docs/Release Notes/!!!meta.json +++ b/docs/Release Notes/!!!meta.json @@ -1,6 +1,6 @@ { "formatVersion": 2, - "appVersion": "0.99.1", + "appVersion": "0.99.2", "files": [ { "isClone": false, @@ -61,6 +61,32 @@ "attachments": [], "dirFileName": "Release Notes", "children": [ + { + "isClone": false, + "noteId": "yuroLztFfpu5", + "notePath": [ + "hD3V4hiu2VW4", + "yuroLztFfpu5" + ], + "title": "v0.99.3", + "notePosition": 10, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/html", + "attributes": [ + { + "type": "relation", + "name": "template", + "value": "wyurrlcDl416", + "isInheritable": false, + "position": 60 + } + ], + "format": "markdown", + "dataFileName": "v0.99.3.md", + "attachments": [] + }, { "isClone": false, "noteId": "z207sehwMJ6C", @@ -69,7 +95,7 @@ "z207sehwMJ6C" ], "title": "v0.99.2", - "notePosition": 10, + "notePosition": 20, "prefix": null, "isExpanded": false, "type": "text", @@ -95,7 +121,7 @@ "WGQsXq2jNyTi" ], "title": "v0.99.1", - "notePosition": 20, + "notePosition": 30, "prefix": null, "isExpanded": false, "type": "text", @@ -121,7 +147,7 @@ "cyw2Yue9vXf3" ], "title": "v0.99.0", - "notePosition": 30, + "notePosition": 40, "prefix": null, "isExpanded": false, "type": "text", @@ -147,7 +173,7 @@ "QOJwjruOUr4k" ], "title": "v0.98.1", - "notePosition": 40, + "notePosition": 50, "prefix": null, "isExpanded": false, "type": "text", @@ -173,7 +199,7 @@ "PLUoryywi0BC" ], "title": "v0.98.0", - "notePosition": 50, + "notePosition": 60, "prefix": null, "isExpanded": false, "type": "text", @@ -199,7 +225,7 @@ "lvOuiWsLDv8F" ], "title": "v0.97.2", - "notePosition": 60, + "notePosition": 70, "prefix": null, "isExpanded": false, "type": "text", @@ -225,7 +251,7 @@ "OtFZ6Nd9vM3n" ], "title": "v0.97.1", - "notePosition": 70, + "notePosition": 80, "prefix": null, "isExpanded": false, "type": "text", @@ -251,7 +277,7 @@ "SJZ5PwfzHSQ1" ], "title": "v0.97.0", - "notePosition": 80, + "notePosition": 90, "prefix": null, "isExpanded": false, "type": "text", @@ -277,7 +303,7 @@ "mYXFde3LuNR7" ], "title": "v0.96.0", - "notePosition": 90, + "notePosition": 100, "prefix": null, "isExpanded": false, "type": "text", @@ -303,7 +329,7 @@ "jthwbL0FdaeU" ], "title": "v0.95.0", - "notePosition": 100, + "notePosition": 110, "prefix": null, "isExpanded": false, "type": "text", @@ -329,7 +355,7 @@ "7HGYsJbLuhnv" ], "title": "v0.94.1", - "notePosition": 110, + "notePosition": 120, "prefix": null, "isExpanded": false, "type": "text", @@ -355,7 +381,7 @@ "Neq53ujRGBqv" ], "title": "v0.94.0", - "notePosition": 120, + "notePosition": 130, "prefix": null, "isExpanded": false, "type": "text", @@ -381,7 +407,7 @@ "VN3xnce1vLkX" ], "title": "v0.93.0", - "notePosition": 130, + "notePosition": 140, "prefix": null, "isExpanded": false, "type": "text", @@ -399,7 +425,7 @@ "WRaBfQqPr6qo" ], "title": "v0.92.7", - "notePosition": 140, + "notePosition": 150, "prefix": null, "isExpanded": false, "type": "text", @@ -425,7 +451,7 @@ "a2rwfKNmUFU1" ], "title": "v0.92.6", - "notePosition": 150, + "notePosition": 160, "prefix": null, "isExpanded": false, "type": "text", @@ -443,7 +469,7 @@ "fEJ8qErr0BKL" ], "title": "v0.92.5-beta", - "notePosition": 160, + "notePosition": 170, "prefix": null, "isExpanded": false, "type": "text", @@ -461,7 +487,7 @@ "kkkZQQGSXjwy" ], "title": "v0.92.4", - "notePosition": 170, + "notePosition": 180, "prefix": null, "isExpanded": false, "type": "text", @@ -479,7 +505,7 @@ "vAroNixiezaH" ], "title": "v0.92.3-beta", - "notePosition": 180, + "notePosition": 190, "prefix": null, "isExpanded": false, "type": "text", @@ -497,7 +523,7 @@ "mHEq1wxAKNZd" ], "title": "v0.92.2-beta", - "notePosition": 190, + "notePosition": 200, "prefix": null, "isExpanded": false, "type": "text", @@ -515,7 +541,7 @@ "IykjoAmBpc61" ], "title": "v0.92.1-beta", - "notePosition": 200, + "notePosition": 210, "prefix": null, "isExpanded": false, "type": "text", @@ -533,7 +559,7 @@ "dq2AJ9vSBX4Y" ], "title": "v0.92.0-beta", - "notePosition": 210, + "notePosition": 220, "prefix": null, "isExpanded": false, "type": "text", @@ -551,7 +577,7 @@ "3a8aMe4jz4yM" ], "title": "v0.91.6", - "notePosition": 220, + "notePosition": 230, "prefix": null, "isExpanded": false, "type": "text", @@ -569,7 +595,7 @@ "8djQjkiDGESe" ], "title": "v0.91.5", - "notePosition": 230, + "notePosition": 240, "prefix": null, "isExpanded": false, "type": "text", @@ -587,7 +613,7 @@ "OylxVoVJqNmr" ], "title": "v0.91.4-beta", - "notePosition": 240, + "notePosition": 250, "prefix": null, "isExpanded": false, "type": "text", @@ -605,7 +631,7 @@ "tANGQDvnyhrj" ], "title": "v0.91.3-beta", - "notePosition": 250, + "notePosition": 260, "prefix": null, "isExpanded": false, "type": "text", @@ -623,7 +649,7 @@ "hMoBfwSoj1SC" ], "title": "v0.91.2-beta", - "notePosition": 260, + "notePosition": 270, "prefix": null, "isExpanded": false, "type": "text", @@ -641,7 +667,7 @@ "a2XMSKROCl9z" ], "title": "v0.91.1-beta", - "notePosition": 270, + "notePosition": 280, "prefix": null, "isExpanded": false, "type": "text", @@ -659,7 +685,7 @@ "yqXFvWbLkuMD" ], "title": "v0.90.12", - "notePosition": 280, + "notePosition": 290, "prefix": null, "isExpanded": false, "type": "text", @@ -677,7 +703,7 @@ "veS7pg311yJP" ], "title": "v0.90.11-beta", - "notePosition": 290, + "notePosition": 300, "prefix": null, "isExpanded": false, "type": "text", @@ -695,7 +721,7 @@ "sq5W9TQxRqMq" ], "title": "v0.90.10-beta", - "notePosition": 300, + "notePosition": 310, "prefix": null, "isExpanded": false, "type": "text", @@ -713,7 +739,7 @@ "yFEGVCUM9tPx" ], "title": "v0.90.9-beta", - "notePosition": 310, + "notePosition": 320, "prefix": null, "isExpanded": false, "type": "text", @@ -731,7 +757,7 @@ "o4wAGqOQuJtV" ], "title": "v0.90.8", - "notePosition": 320, + "notePosition": 330, "prefix": null, "isExpanded": false, "type": "text", @@ -764,7 +790,7 @@ "i4A5g9iOg9I0" ], "title": "v0.90.7-beta", - "notePosition": 330, + "notePosition": 340, "prefix": null, "isExpanded": false, "type": "text", @@ -782,7 +808,7 @@ "ThNf2GaKgXUs" ], "title": "v0.90.6-beta", - "notePosition": 340, + "notePosition": 350, "prefix": null, "isExpanded": false, "type": "text", @@ -800,7 +826,7 @@ "G4PAi554kQUr" ], "title": "v0.90.5-beta", - "notePosition": 350, + "notePosition": 360, "prefix": null, "isExpanded": false, "type": "text", @@ -827,7 +853,7 @@ "zATRobGRCmBn" ], "title": "v0.90.4", - "notePosition": 360, + "notePosition": 370, "prefix": null, "isExpanded": false, "type": "text", @@ -845,7 +871,7 @@ "sCDLf8IKn3Iz" ], "title": "v0.90.3", - "notePosition": 370, + "notePosition": 380, "prefix": null, "isExpanded": false, "type": "text", @@ -863,7 +889,7 @@ "VqqyBu4AuTjC" ], "title": "v0.90.2-beta", - "notePosition": 380, + "notePosition": 390, "prefix": null, "isExpanded": false, "type": "text", @@ -881,7 +907,7 @@ "RX3Nl7wInLsA" ], "title": "v0.90.1-beta", - "notePosition": 390, + "notePosition": 400, "prefix": null, "isExpanded": false, "type": "text", @@ -899,7 +925,7 @@ "GyueACukPWjk" ], "title": "v0.90.0-beta", - "notePosition": 400, + "notePosition": 410, "prefix": null, "isExpanded": false, "type": "text", @@ -917,7 +943,7 @@ "wyurrlcDl416" ], "title": "Release Template", - "notePosition": 410, + "notePosition": 420, "prefix": null, "isExpanded": false, "type": "text", diff --git a/docs/Release Notes/Release Notes/Release Template.md b/docs/Release Notes/Release Notes/Release Template.md index acba4dc76..d1371c558 100644 --- a/docs/Release Notes/Release Notes/Release Template.md +++ b/docs/Release Notes/Release Notes/Release Template.md @@ -1,4 +1,7 @@ # Release Template +> [!NOTE] +> If you are interested in an [official mobile application](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/7447)  ([#7447](https://github.com/TriliumNext/Trilium/issues/7447)) or [multi-user support](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/4956) ([#4956](https://github.com/TriliumNext/Trilium/issues/4956)), consider offering financial support via IssueHunt (see links). + > [!IMPORTANT] > If you enjoyed this release, consider showing a token of appreciation by: > diff --git a/docs/Release Notes/Release Notes/v0.99.3.md b/docs/Release Notes/Release Notes/v0.99.3.md new file mode 100644 index 000000000..e82ddaf60 --- /dev/null +++ b/docs/Release Notes/Release Notes/v0.99.3.md @@ -0,0 +1,36 @@ +# v0.99.3 +> [!NOTE] +> If you are interested in an [official mobile application](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/7447)  ([#7447](https://github.com/TriliumNext/Trilium/issues/7447)) or [multi-user support](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/4956) ([#4956](https://github.com/TriliumNext/Trilium/issues/4956)), consider offering financial support via IssueHunt (see links). + +> [!IMPORTANT] +> If you enjoyed this release, consider showing a token of appreciation by: +> +> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right). +> * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran). + +## 🐞 Bugfixes + +* [Issues with the ribbon and some about dialogs when formatting locale is set to Chinese](https://github.com/TriliumNext/Trilium/issues/7444) +* Incorrect date format for Chinese. +* Development locale appearing in production. +* Split reverted on right-to-left languages +* Fixes for the Flatpak installation method: + * [Flatpak Data Access Issue on install/update : permission issue](https://github.com/TriliumNext/Trilium/issues/7454) + * [flatpak doesn't launch after install, needs no-sandbox](https://github.com/TriliumNext/Trilium/issues/5516) + * Icon missing in Flatpak shortcut. +* [Trilium does not unlock after autolock](https://github.com/TriliumNext/Trilium/issues/7448) by @perfectra1n +* Note color not taken into consideration for reference links by @adoriandoran +* [Board view doesn't respond to Color tags](https://github.com/TriliumNext/Trilium/issues/7456) by @adoriandoran + +## 📖 Documentation + +* Improve links & starting page in demo note by @tredondo + +## 🌍 Internationalization + +* Added support for Italian +* Various translation improvements. + +## 🛠️ Technical updates + +* Various dependency updates. \ No newline at end of file diff --git a/package.json b/package.json index d974c5252..54d96c346 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/source", - "version": "0.99.2", + "version": "0.99.3", "description": "Build your personal knowledge base with Trilium Notes", "directories": { "doc": "docs" diff --git a/packages/commons/package.json b/packages/commons/package.json index 96abcb2d0..ecb5a08c3 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/commons", - "version": "0.99.2", + "version": "0.99.3", "description": "Shared library between the clients (e.g. browser, Electron) and the server, mostly for type definitions and utility methods.", "private": true, "type": "module", From 347da8abdecddaf997915c01ec76d9274ac5c0e3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 08:16:26 +0300 Subject: [PATCH 055/250] fix(collections/list): excessive spacing in empty note content (closes #7319) --- .../src/widgets/collections/legacy/ListOrGridView.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx index d29d7e275..2b5d1bdd0 100644 --- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx +++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx @@ -141,7 +141,11 @@ function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note: }) .then(({ $renderedContent, type }) => { if (!contentRef.current) return; - contentRef.current.replaceChildren(...$renderedContent); + if ($renderedContent[0].innerHTML) { + contentRef.current.replaceChildren(...$renderedContent); + } else { + contentRef.current.replaceChildren(); + } contentRef.current.classList.add(`type-${type}`); highlightSearch(contentRef.current); }) From 94d62f810a5748b6dececf7b78561a242e084da2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:06:13 +0300 Subject: [PATCH 056/250] fix(share): heading and navigation not supporting CJK (closes #6430) --- apps/server/src/services/utils.ts | 8 ++++++++ apps/server/src/share/routes.ts | 5 +++-- packages/share-theme/src/templates/page.ejs | 1 - packages/share-theme/src/templates/toc_item.ejs | 1 - 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index ca6b809bb..75d6b564c 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -497,6 +497,14 @@ export function formatSize(size: number | null | undefined) { } } +export function slugify(text: string) { + return text + .normalize("NFKD") // handles accents like é → e + .toLowerCase() + .replace(/[^\p{Letter}\p{Number}]+/gu, "-") // keep Unicode letters/numbers + .replace(/(^-|-$)+/g, ""); // trim leading/trailing dashes +} + export default { compareVersions, crash, diff --git a/apps/server/src/share/routes.ts b/apps/server/src/share/routes.ts index 37162ea28..f8bce49b0 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -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, { isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; +import utils, { isDev, safeExtractMessageAndStackFromError, slugify } from "../services/utils.js"; import options from "../services/options.js"; import { t } from "i18next"; import ejs from "ejs"; @@ -175,7 +175,8 @@ function register(router: Router) { appPath: isDev ? appPath : `../${appPath}`, showLoginInShareTheme, t, - isDev + isDev, + slugify }; let useDefaultView = true; diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index cc96cc4ca..0a227a32c 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -90,7 +90,6 @@ const currentTheme = note.getLabel("shareTheme") === "light" ? "light" : "dark"; const themeClass = currentTheme === "light" ? " theme-light" : " theme-dark"; const headingRe = /()(.+?)(<\/h[1-6]>)/g; const headingMatches = [...content.matchAll(headingRe)]; -const slugify = (text) => text.toLowerCase().replace(/[^\w]/g, "-"); content = content.replaceAll(headingRe, (...match) => { match[0] = match[0].replace(match[3], `#${match[3]}`); return match[0]; diff --git a/packages/share-theme/src/templates/toc_item.ejs b/packages/share-theme/src/templates/toc_item.ejs index b18b4a1a6..726ca4cca 100644 --- a/packages/share-theme/src/templates/toc_item.ejs +++ b/packages/share-theme/src/templates/toc_item.ejs @@ -1,5 +1,4 @@ <% -const slugify = (text) => text.toLowerCase().replace(/[^\w]/g, "-"); const slug = slugify(entry.name); %> From d2b6014b499f774c4edc9e4d015d571736ee90d9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:19:54 +0300 Subject: [PATCH 057/250] fix(share): HTML tags displayed escaped in headings --- apps/server/src/services/utils.ts | 3 ++- apps/server/src/share/routes.ts | 4 ++-- packages/share-theme/src/templates/page.ejs | 3 ++- packages/share-theme/src/templates/toc_item.ejs | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index 75d6b564c..8a4d7c7aa 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -497,7 +497,7 @@ export function formatSize(size: number | null | undefined) { } } -export function slugify(text: string) { +function slugify(text: string) { return text .normalize("NFKD") // handles accents like é → e .toLowerCase() @@ -540,6 +540,7 @@ export default { safeExtractMessageAndStackFromError, sanitizeSqlIdentifier, stripTags, + slugify, timeLimit, toBase64, toMap, diff --git a/apps/server/src/share/routes.ts b/apps/server/src/share/routes.ts index f8bce49b0..77f542ba2 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -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, { isDev, safeExtractMessageAndStackFromError, slugify } from "../services/utils.js"; +import utils, { isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; import options from "../services/options.js"; import { t } from "i18next"; import ejs from "ejs"; @@ -176,7 +176,7 @@ function register(router: Router) { showLoginInShareTheme, t, isDev, - slugify + utils }; let useDefaultView = true; diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index 0a227a32c..2fd07c8a7 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -91,7 +91,8 @@ const themeClass = currentTheme === "light" ? " theme-light" : " theme-dark"; const headingRe = /()(.+?)(<\/h[1-6]>)/g; const headingMatches = [...content.matchAll(headingRe)]; content = content.replaceAll(headingRe, (...match) => { - match[0] = match[0].replace(match[3], `#${match[3]}`); + const slug = utils.slugify(utils.stripTags(match[2])); + match[0] = match[0].replace(match[3], `#${match[3]}`); return match[0]; }); %> diff --git a/packages/share-theme/src/templates/toc_item.ejs b/packages/share-theme/src/templates/toc_item.ejs index 726ca4cca..4346fe55a 100644 --- a/packages/share-theme/src/templates/toc_item.ejs +++ b/packages/share-theme/src/templates/toc_item.ejs @@ -1,11 +1,11 @@ <% -const slug = slugify(entry.name); +const strippedName = utils.stripTags(entry.name); +const slug = utils.slugify(strippedName); %> -
  • - <%= entry.name %> + <%= strippedName %> <% if (entry.children.length) { %> From 0fa1c0f5c40870d178f145e74e516fc165db0299 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:26:15 +0300 Subject: [PATCH 058/250] chore(share): improve slugification to strip diacritics cleanly --- apps/server/src/services/utils.spec.ts | 30 ++++++++++++++++++++++++++ apps/server/src/services/utils.ts | 3 ++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/utils.spec.ts b/apps/server/src/services/utils.spec.ts index 6e027b7bd..b7e57b441 100644 --- a/apps/server/src/services/utils.spec.ts +++ b/apps/server/src/services/utils.spec.ts @@ -681,3 +681,33 @@ describe("#normalizeCustomHandlerPattern", () => { }); }); }); + +describe("#slugify", () => { + it("should return a slugified string", () => { + const testString = "This is a Test String! With unicode & Special #Chars."; + const expectedSlug = "this-is-a-test-string-with-unicode-special-chars"; + const result = utils.slugify(testString); + expect(result).toBe(expectedSlug); + }); + + it("supports CJK characters without alteration", () => { + const testString = "测试中文字符"; + const expectedSlug = "测试中文字符"; + const result = utils.slugify(testString); + expect(result).toBe(expectedSlug); + }); + + it("supports Cyrillic characters without alteration", () => { + const testString = "Тестирование кириллических символов"; + const expectedSlug = "тестирование-кириллических-символов"; + const result = utils.slugify(testString); + expect(result).toBe(expectedSlug); + }); + + it("removes diacritic marks from characters", () => { + const testString = "Café naïve façade jalapeño"; + const expectedSlug = "cafe-naive-facade-jalapeno"; + const result = utils.slugify(testString); + expect(result).toBe(expectedSlug); + }); +}); diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index 8a4d7c7aa..cdaa9d719 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -499,7 +499,8 @@ export function formatSize(size: number | null | undefined) { function slugify(text: string) { return text - .normalize("NFKD") // handles accents like é → e + .normalize("NFKD") // decompose accents + .replace(/\p{Mark}/gu, "") // remove diacritics cleanly .toLowerCase() .replace(/[^\p{Letter}\p{Number}]+/gu, "-") // keep Unicode letters/numbers .replace(/(^-|-$)+/g, ""); // trim leading/trailing dashes From aae90ede192b5e658d32b870f375d49a52c3ba41 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:29:47 +0300 Subject: [PATCH 059/250] chore(share): keep diacritics in slug instead of stripping them in --- apps/server/src/services/utils.spec.ts | 5 +++-- apps/server/src/services/utils.ts | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/server/src/services/utils.spec.ts b/apps/server/src/services/utils.spec.ts index b7e57b441..e815de3f8 100644 --- a/apps/server/src/services/utils.spec.ts +++ b/apps/server/src/services/utils.spec.ts @@ -704,9 +704,10 @@ describe("#slugify", () => { expect(result).toBe(expectedSlug); }); - it("removes diacritic marks from characters", () => { + // preserves diacritic marks + it("preserves diacritic marks", () => { const testString = "Café naïve façade jalapeño"; - const expectedSlug = "cafe-naive-facade-jalapeno"; + const expectedSlug = "café-naïve-façade-jalapeño"; const result = utils.slugify(testString); expect(result).toBe(expectedSlug); }); diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index cdaa9d719..6d567f15a 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -499,11 +499,10 @@ export function formatSize(size: number | null | undefined) { function slugify(text: string) { return text - .normalize("NFKD") // decompose accents - .replace(/\p{Mark}/gu, "") // remove diacritics cleanly + .normalize("NFC") // keep composed form, preserves accents .toLowerCase() - .replace(/[^\p{Letter}\p{Number}]+/gu, "-") // keep Unicode letters/numbers - .replace(/(^-|-$)+/g, ""); // trim leading/trailing dashes + .replace(/[^\p{Letter}\p{Number}]+/gu, "-") // replace non-letter/number with "-" + .replace(/(^-|-$)+/g, ""); // trim dashes } export default { From af95d85b734831217ff9fd5608a27e7f17a1e2df Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:43:04 +0300 Subject: [PATCH 060/250] chore(client): disable import/export for help notes --- apps/client/src/widgets/ribbon/NoteActions.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index 14adc6b4b..1c1c17502 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -46,7 +46,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not const parentComponent = useContext(ParentComponent); const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type); - const isInOptions = note.noteId.startsWith("_options"); + const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help"); const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && note.getLabelValue("viewType") === "presentation"); const isElectron = getIsElectron(); const isMac = getIsMac(); @@ -69,10 +69,10 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} /> noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", { notePath: noteContext.notePath, defaultType: "single" @@ -84,14 +84,14 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not - + branches.deleteNotes([note.getParentBranches()[0].branchId])} /> - + ); } From 5d0669b4640e4209adbcf5da8f542356c721889b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 16:11:05 +0300 Subject: [PATCH 061/250] fix(canvas): images appearing muted in dark mode (closes #5708) --- apps/client/src/stylesheets/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index fd8383130..7f67e4827 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -2432,4 +2432,8 @@ iframe.print-iframe { bottom: 0; width: 0; height: 0; +} + +.excalidraw.theme--dark canvas { + --theme-filter: invert(100%) hue-rotate(180deg); } \ No newline at end of file From e94b5ac07abd79cb62ad5206b44f81bf3dbc5881 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 16:42:27 +0300 Subject: [PATCH 062/250] fix(server): edited notes listing automatically generated hidden notes (closes #5683) --- apps/server/src/routes/api/revisions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/server/src/routes/api/revisions.ts b/apps/server/src/routes/api/revisions.ts index 055d0b75e..d126558f0 100644 --- a/apps/server/src/routes/api/revisions.ts +++ b/apps/server/src/routes/api/revisions.ts @@ -152,14 +152,14 @@ function restoreRevision(req: Request) { } function getEditedNotesOnDate(req: Request) { - const noteIds = sql.getColumn( - ` + const noteIds = sql.getColumn(/*sql*/`\ SELECT notes.* FROM notes WHERE noteId IN ( SELECT noteId FROM notes - WHERE notes.dateCreated LIKE :date - OR notes.dateModified LIKE :date + WHERE + (notes.dateCreated LIKE :date OR notes.dateModified LIKE :date) + AND (noteId NOT LIKE '_%') UNION ALL SELECT noteId FROM revisions WHERE revisions.dateLastEdited LIKE :date From f95082ccdb17c0b9145aa70f871098460e6029f7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 16:57:48 +0300 Subject: [PATCH 063/250] fix(server): calendar tooltip positioning (closes #5675) --- apps/client/src/widgets/buttons/right_dropdown_button.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/client/src/widgets/buttons/right_dropdown_button.ts b/apps/client/src/widgets/buttons/right_dropdown_button.ts index 6d896eae2..7c43f14af 100644 --- a/apps/client/src/widgets/buttons/right_dropdown_button.ts +++ b/apps/client/src/widgets/buttons/right_dropdown_button.ts @@ -47,8 +47,9 @@ export default class RightDropdownButtonWidget extends BasicWidget { } }); - this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title); - this.tooltip = new Tooltip(this.$tooltip[0], { + this.$widget.attr("title", this.title); + this.tooltip = Tooltip.getOrCreateInstance(this.$widget[0], { + trigger: "hover", placement: handleRightToLeftPlacement(this.settings.titlePlacement), fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ] }); @@ -56,9 +57,7 @@ export default class RightDropdownButtonWidget extends BasicWidget { this.$widget .find(".right-dropdown-button") .addClass(this.iconClass) - .on("click", () => this.tooltip.hide()) - .on("mouseenter", () => this.tooltip.show()) - .on("mouseleave", () => this.tooltip.hide()); + .on("click", () => this.tooltip.hide()); this.$widget.on("show.bs.dropdown", async () => { await this.dropdownShown(); From ab6da26a25a1f4880bebb3fd737eb029b8239a7c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 18:09:32 +0300 Subject: [PATCH 064/250] refactor(client): standalone rendering mechanism for ribbon --- apps/client/src/layouts/layout_commons.tsx | 5 +- .../src/widgets/ribbon/FormattingToolbar.tsx | 13 +- apps/client/src/widgets/ribbon/Ribbon.tsx | 156 +----------------- .../src/widgets/ribbon/RibbonDefinition.ts | 134 +++++++++++++++ .../components/StandaloneRibbonAdapter.tsx | 43 +++++ .../src/widgets/ribbon/ribbon-interface.ts | 19 +++ 6 files changed, 207 insertions(+), 163 deletions(-) create mode 100644 apps/client/src/widgets/ribbon/RibbonDefinition.ts create mode 100644 apps/client/src/widgets/ribbon/components/StandaloneRibbonAdapter.tsx diff --git a/apps/client/src/layouts/layout_commons.tsx b/apps/client/src/layouts/layout_commons.tsx index 610f31dda..26f8ea232 100644 --- a/apps/client/src/layouts/layout_commons.tsx +++ b/apps/client/src/layouts/layout_commons.tsx @@ -29,8 +29,9 @@ import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import NoteDetailWidget from "../widgets/note_detail.js"; import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx"; import NoteTitleWidget from "../widgets/note_title.jsx"; -import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js"; +import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js"; import NoteList from "../widgets/collections/NoteList.jsx"; +import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; export function applyModals(rootContainer: RootContainer) { rootContainer @@ -63,7 +64,7 @@ export function applyModals(rootContainer: RootContainer) { .cssBlock(".title-row > * { margin: 5px; }") .child() .child()) - .child() + .child() .child(new PromotedAttributesWidget()) .child(new NoteDetailWidget()) .child()) diff --git a/apps/client/src/widgets/ribbon/FormattingToolbar.tsx b/apps/client/src/widgets/ribbon/FormattingToolbar.tsx index 3282ce5af..8828b15ee 100644 --- a/apps/client/src/widgets/ribbon/FormattingToolbar.tsx +++ b/apps/client/src/widgets/ribbon/FormattingToolbar.tsx @@ -1,4 +1,5 @@ -import { useNoteContext, useTriliumOption } from "../react/hooks"; +import { useTriliumOption } from "../react/hooks"; +import { TabContext } from "./ribbon-interface"; /** * Handles the editing toolbar when the CKEditor is in decoupled mode. @@ -6,19 +7,13 @@ import { useNoteContext, useTriliumOption } from "../react/hooks"; * This toolbar is only enabled if the user has selected the classic CKEditor. * * The ribbon item is active by default for text notes, as long as they are not in read-only mode. - * + * * ! The toolbar is not only used in the ribbon, but also in the quick edit feature. */ -export default function FormattingToolbar({ hidden }: { hidden?: boolean }) { +export default function FormattingToolbar({ hidden }: TabContext) { const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType"); return (textNoteEditorType === "ckeditor-classic" &&
    ) }; - -export function PopupEditorFormattingToolbar() { - // TODO: Integrate this directly once we migrate away from class components. - const { note } = useNoteContext(); - return