From 2c6ba9ba2cbc8b7eff96560f25919f8ffa64010e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 13 Jun 2025 17:42:11 +0300 Subject: [PATCH 001/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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/190] 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 aa102ab3939f5c30b0afa4635fc5dc8dd0079ce2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 14:54:20 +0300 Subject: [PATCH 032/190] fix(export/share): missing templates after merge --- apps/server/src/services/export/zip/html.ts | 9 +++++++-- apps/server/src/share/content_renderer.ts | 13 ++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/server/src/services/export/zip/html.ts b/apps/server/src/services/export/zip/html.ts index 8eb5c5d93..259e4da67 100644 --- a/apps/server/src/services/export/zip/html.ts +++ b/apps/server/src/services/export/zip/html.ts @@ -1,8 +1,9 @@ import type NoteMeta from "../../meta/note_meta.js"; -import { escapeHtml } from "../../utils"; -import cssContent from "@triliumnext/ckeditor5/content.css"; +import { escapeHtml, getResourceDir, isDev } from "../../utils"; import html from "html"; import { ZipExportProvider } from "./abstract_provider.js"; +import path from "path"; +import fs from "fs"; export default class HtmlExportProvider extends ZipExportProvider { @@ -164,6 +165,10 @@ export default class HtmlExportProvider extends ZipExportProvider { return; } + const cssFile = isDev + ? path.join(__dirname, "../../../../../node_modules/ckeditor5/dist/ckeditor5-content.css") + : path.join(getResourceDir(), "ckeditor5-content.css"); + const cssContent = fs.readFileSync(cssFile, "utf-8"); this.archive.append(cssContent, { name: cssMeta.dataFileName }); } diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 31c3896c6..6a45b66e5 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -189,15 +189,22 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) } // Render with the default view otherwise. - const templatePath = join(getResourceDir(), "share-theme", "templates", "page.ejs"); + const templatePath = getDefaultTemplatePath("page"); return ejs.render(readTemplate(templatePath), opts, { includer: (path) => { - const templatePath = join(getResourceDir(), "share-theme", "templates", `${path}.ejs`); - return { template: readTemplate(templatePath) }; + // Path is relative to apps/server/dist/assets/views + return { template: readTemplate(getDefaultTemplatePath(path)) }; } }); } +function getDefaultTemplatePath(template: string) { + // Path is relative to apps/server/dist/assets/views + return process.env.NODE_ENV === "development" + ? join(__dirname, `../../../../packages/share-theme/src/templates/${template}.ejs`) + : `../../share-theme/templates/${template}.ejs`; +} + function readTemplate(path: string) { const cachedTemplate = templateCache.get(path); if (cachedTemplate) { From bb636128b049da91a2567f0c9186cac023044bb1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 16:02:20 +0300 Subject: [PATCH 033/190] fix(export/share): missing files and wrong meta handling --- apps/server/src/services/export/zip.ts | 20 +------------------- apps/server/src/services/export/zip/html.ts | 2 +- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 0ad0d7f22..8733686a9 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -3,7 +3,7 @@ import dateUtils from "../date_utils.js"; import path from "path"; import packageInfo from "../../../package.json" with { type: "json" }; -import { getContentDisposition, escapeHtml, getResourceDir, isDev } from "../utils.js"; +import { getContentDisposition } from "../utils.js"; import protectedSessionService from "../protected_session.js"; import sanitize from "sanitize-filename"; import fs from "fs"; @@ -415,24 +415,6 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch, } return; } - - const metaFileJson = JSON.stringify(metaFile, null, "\t"); - - archive.append(metaFileJson, { name: "!!!meta.json" }); - - saveNote(rootMeta, ""); - - const note = branch.getNote(); - const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected() || "note"}.zip`; - - if (setHeaders && "setHeader" in res) { - res.setHeader("Content-Disposition", getContentDisposition(zipFileName)); - res.setHeader("Content-Type", "application/zip"); - } - - archive.pipe(res); - await archive.finalize(); - taskContext.taskSucceeded(null); } catch (e: unknown) { const message = `Export failed with error: ${e instanceof Error ? e.message : String(e)}`; log.error(message); diff --git a/apps/server/src/services/export/zip/html.ts b/apps/server/src/services/export/zip/html.ts index 259e4da67..0cab8193c 100644 --- a/apps/server/src/services/export/zip/html.ts +++ b/apps/server/src/services/export/zip/html.ts @@ -166,7 +166,7 @@ export default class HtmlExportProvider extends ZipExportProvider { } const cssFile = isDev - ? path.join(__dirname, "../../../../../node_modules/ckeditor5/dist/ckeditor5-content.css") + ? path.join(__dirname, "../../../../../../node_modules/ckeditor5/dist/ckeditor5-content.css") : path.join(getResourceDir(), "ckeditor5-content.css"); const cssContent = fs.readFileSync(cssFile, "utf-8"); this.archive.append(cssContent, { name: cssMeta.dataFileName }); From 357d294f2d349f5aa5fcd8f33e3ae82d4cce9f7d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 18:25:16 +0300 Subject: [PATCH 034/190] chore(export/share): address review --- apps/server/src/services/export/zip.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index 8733686a9..f7ef70bc8 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -27,14 +27,14 @@ import { NoteType } from "@triliumnext/commons"; async function exportToZip(taskContext: TaskContext<"export">, 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`); + throw new ValidationError(`Only 'html', 'markdown' and 'share' allowed as export format, '${format}' given`); } const archive = archiver("zip", { zlib: { level: 9 } // Sets the compression level. }); const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks); - const provider= buildProvider(); + const provider = buildProvider(); const noteIdToMeta: Record = {}; From 3660e2f12799c471099da03a7d622adbcff5bcd5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 19:00:26 +0300 Subject: [PATCH 035/190] refactor(share): store assets at /share/asset level --- apps/server/scripts/build.ts | 1 + apps/server/src/routes/assets.ts | 2 ++ apps/server/src/share/content_renderer.ts | 13 +++++-------- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/server/scripts/build.ts b/apps/server/scripts/build.ts index d2ed99ee2..6e0c53adf 100644 --- a/apps/server/scripts/build.ts +++ b/apps/server/scripts/build.ts @@ -7,6 +7,7 @@ async function main() { // Copy assets build.copy("src/assets", "assets/"); + build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/"); build.copy("/packages/share-theme/src/templates", "share-theme/templates/"); // Copy node modules dependencies diff --git a/apps/server/src/routes/assets.ts b/apps/server/src/routes/assets.ts index a1a2bfb63..efcb5a335 100644 --- a/apps/server/src/routes/assets.ts +++ b/apps/server/src/routes/assets.ts @@ -32,6 +32,7 @@ async function register(app: express.Application) { req.url = `/${assetUrlFragment}` + req.url; vite.middlewares(req, res, next); }); + app.use(`/share/assets/`, express.static(path.join(srcRoot, "../../packages/share-theme/dist"))); } else { const publicDir = path.join(resourceDir, "public"); if (!existsSync(publicDir)) { @@ -42,6 +43,7 @@ async function register(app: express.Application) { app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(publicDir, "stylesheets"))); app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(publicDir, "fonts"))); app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(publicDir, "translations"))); + app.use(`/share/assets/`, persistentCacheStatic(path.join(resourceDir, "share-theme/assets"))); app.use(`/node_modules/`, persistentCacheStatic(path.join(publicDir, "node_modules"))); } app.use(`/${assetUrlFragment}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images"))); diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 6a45b66e5..aa42ecf44 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -9,15 +9,13 @@ 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"; -import utils, { getResourceDir, isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; -import app_path from "../services/app_path.js"; +import utils, { isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; import ejs from "ejs"; import log from "../services/log.js"; import { join } from "path"; import { readFileSync } from "fs"; const shareAdjustedAssetPath = isDev ? assetPath : `../${assetPath}`; -const shareAdjustedAppPath = isDev ? app_path : `../${app_path}`; const templateCache: Map = new Map(); /** @@ -99,9 +97,9 @@ export function renderNoteContent(note: SNote) { // Determine CSS to load. const cssToLoad: string[] = []; - if (!isDev && !note.isLabelTruthy("shareOmitDefaultCss")) { - cssToLoad.push(`${shareAdjustedAssetPath}/src/share.css`); - cssToLoad.push(`${shareAdjustedAssetPath}/src/boxicons.css`); + if (!note.isLabelTruthy("shareOmitDefaultCss")) { + cssToLoad.push(`assets/styles.css`); + cssToLoad.push(`assets/boxicons.css`); } for (const cssRelation of note.getRelations("shareCss")) { cssToLoad.push(`api/notes/${cssRelation.value}/download`); @@ -109,7 +107,7 @@ export function renderNoteContent(note: SNote) { // Determine JS to load. const jsToLoad: string[] = [ - `${shareAdjustedAppPath}/share.js` + "assets/scripts.js" ]; for (const jsRelation of note.getRelations("shareJs")) { jsToLoad.push(`api/notes/${jsRelation.value}/download`); @@ -147,7 +145,6 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) isEmpty, assetPath: shareAdjustedAssetPath, assetUrlFragment, - appPath: shareAdjustedAppPath, showLoginInShareTheme, t, isDev, From 4ef766748440891f1182e87a6d8568f84ab10933 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 19:09:19 +0300 Subject: [PATCH 036/190] chore(share): bring back inline mermaid rendering --- apps/client/src/share.ts | 3 --- packages/share-theme/package.json | 3 +++ packages/share-theme/src/scripts/index.ts | 2 ++ .../share-theme/src/scripts/modules}/mermaid.ts | 0 pnpm-lock.yaml | 8 ++++++++ 5 files changed, 13 insertions(+), 3 deletions(-) rename {apps/client/src/share => packages/share-theme/src/scripts/modules}/mermaid.ts (100%) diff --git a/apps/client/src/share.ts b/apps/client/src/share.ts index b438f0c0c..d073baca1 100644 --- a/apps/client/src/share.ts +++ b/apps/client/src/share.ts @@ -32,9 +32,6 @@ async function formatCodeBlocks() { async function setupTextNote() { formatCodeBlocks(); applyMath(); - - const setupMermaid = (await import("./share/mermaid.js")).default; - setupMermaid(); } /** diff --git a/packages/share-theme/package.json b/packages/share-theme/package.json index a521b66aa..515a6fc02 100644 --- a/packages/share-theme/package.json +++ b/packages/share-theme/package.json @@ -21,6 +21,9 @@ "Zerebos " ], "license": "Apache-2.0", + "dependencies": { + "mermaid": "11.12.0" + }, "devDependencies": { "@digitak/esrun": "3.2.26", "@types/swagger-ui": "5.21.1", diff --git a/packages/share-theme/src/scripts/index.ts b/packages/share-theme/src/scripts/index.ts index 7b71fcb8f..03c5c101c 100644 --- a/packages/share-theme/src/scripts/index.ts +++ b/packages/share-theme/src/scripts/index.ts @@ -3,6 +3,7 @@ import setupExpanders from "./modules/expanders"; import setupMobileMenu from "./modules/mobile"; import setupSearch from "./modules/search"; import setupThemeSelector from "./modules/theme"; +import setupMermaid from "./modules/mermaid"; function $try unknown>(func: T, ...args: Parameters) { try { @@ -18,3 +19,4 @@ $try(setupToC); $try(setupExpanders); $try(setupMobileMenu); $try(setupSearch); +$try(setupMermaid); diff --git a/apps/client/src/share/mermaid.ts b/packages/share-theme/src/scripts/modules/mermaid.ts similarity index 100% rename from apps/client/src/share/mermaid.ts rename to packages/share-theme/src/scripts/modules/mermaid.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b3dbf218..e2755aca2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1328,6 +1328,10 @@ importers: version: 1.2.0 packages/share-theme: + dependencies: + mermaid: + specifier: 11.12.0 + version: 11.12.0 devDependencies: '@digitak/esrun': specifier: 3.2.26 @@ -15335,6 +15339,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.1.0': dependencies: @@ -15831,6 +15837,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-restricted-editing@47.1.0': dependencies: From 6ae67c410cfcfbc27d1bca4bb354517c2cfbd70c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 20:52:47 +0300 Subject: [PATCH 037/190] chore(share): load Mermaid only when necessary --- packages/share-theme/src/scripts/modules/mermaid.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/share-theme/src/scripts/modules/mermaid.ts b/packages/share-theme/src/scripts/modules/mermaid.ts index 123f3816c..78ae5a573 100644 --- a/packages/share-theme/src/scripts/modules/mermaid.ts +++ b/packages/share-theme/src/scripts/modules/mermaid.ts @@ -1,7 +1,12 @@ -import mermaid from "mermaid"; +export default async function setupMermaid() { + const mermaidEls = document.querySelectorAll("#content pre code.language-mermaid"); + if (mermaidEls.length === 0) { + return; + } -export default function setupMermaid() { - for (const codeBlock of document.querySelectorAll("#content pre code.language-mermaid")) { + const mermaid = (await import("mermaid")).default; + + for (const codeBlock of mermaidEls) { const parentPre = codeBlock.parentElement; if (!parentPre) { continue; From b9a4e7ab117d7516734017a0ff24a0f2422a3168 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 20:52:54 +0300 Subject: [PATCH 038/190] chore(share): enable code splitting --- packages/share-theme/scripts/build.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/share-theme/scripts/build.ts b/packages/share-theme/scripts/build.ts index 586005ea5..073165f36 100644 --- a/packages/share-theme/scripts/build.ts +++ b/packages/share-theme/scripts/build.ts @@ -1,4 +1,3 @@ -import fs from "node:fs"; import path from "node:path"; // import {fileURLToPath} from "node:url"; @@ -51,8 +50,9 @@ async function runBuild() { await esbuild.build({ entryPoints: entryPoints, bundle: true, + splitting: true, outdir: path.join(rootDir, "dist"), - format: "cjs", + format: "esm", target: ["chrome96"], loader: { ".png": "dataurl", From e3dd25b5918148924de4cf2e2fcf74c0573eb1c1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 21:10:31 +0300 Subject: [PATCH 039/190] chore(share): set up math --- apps/client/src/share.ts | 9 --------- apps/server/src/share/content_renderer.ts | 1 + packages/share-theme/package.json | 1 + packages/share-theme/src/scripts/index.ts | 2 ++ packages/share-theme/src/scripts/modules/math.ts | 13 +++++++++++++ packages/share-theme/src/styles/content.css | 4 ++++ pnpm-lock.yaml | 7 +++---- 7 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 packages/share-theme/src/scripts/modules/math.ts diff --git a/apps/client/src/share.ts b/apps/client/src/share.ts index d073baca1..602f7e95c 100644 --- a/apps/client/src/share.ts +++ b/apps/client/src/share.ts @@ -9,15 +9,7 @@ async function ensureJQuery() { (window as any).$ = $; } -async function applyMath() { - const anyMathBlock = document.querySelector("#content .math-tex"); - if (!anyMathBlock) { - return; - } - const renderMathInElement = (await import("./services/math.js")).renderMathInElement; - renderMathInElement(document.getElementById("content")); -} async function formatCodeBlocks() { const anyCodeBlock = document.querySelector("#content pre"); @@ -31,7 +23,6 @@ async function formatCodeBlocks() { async function setupTextNote() { formatCodeBlocks(); - applyMath(); } /** diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index aa42ecf44..3b5ce0a6d 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -99,6 +99,7 @@ export function renderNoteContent(note: SNote) { const cssToLoad: string[] = []; if (!note.isLabelTruthy("shareOmitDefaultCss")) { cssToLoad.push(`assets/styles.css`); + cssToLoad.push(`assets/scripts.css`); cssToLoad.push(`assets/boxicons.css`); } for (const cssRelation of note.getRelations("shareCss")) { diff --git a/packages/share-theme/package.json b/packages/share-theme/package.json index 515a6fc02..ecaacfb52 100644 --- a/packages/share-theme/package.json +++ b/packages/share-theme/package.json @@ -22,6 +22,7 @@ ], "license": "Apache-2.0", "dependencies": { + "katex": "0.16.25", "mermaid": "11.12.0" }, "devDependencies": { diff --git a/packages/share-theme/src/scripts/index.ts b/packages/share-theme/src/scripts/index.ts index 03c5c101c..fbbc864d0 100644 --- a/packages/share-theme/src/scripts/index.ts +++ b/packages/share-theme/src/scripts/index.ts @@ -4,6 +4,7 @@ import setupMobileMenu from "./modules/mobile"; import setupSearch from "./modules/search"; import setupThemeSelector from "./modules/theme"; import setupMermaid from "./modules/mermaid"; +import setupMath from "./modules/math"; function $try unknown>(func: T, ...args: Parameters) { try { @@ -20,3 +21,4 @@ $try(setupExpanders); $try(setupMobileMenu); $try(setupSearch); $try(setupMermaid); +$try(setupMath); diff --git a/packages/share-theme/src/scripts/modules/math.ts b/packages/share-theme/src/scripts/modules/math.ts new file mode 100644 index 000000000..a1c3195ac --- /dev/null +++ b/packages/share-theme/src/scripts/modules/math.ts @@ -0,0 +1,13 @@ +export default async function setupMath() { + const anyMathBlock = document.querySelector("#content .math-tex"); + if (!anyMathBlock) { + return; + } + + const renderMathInElement = (await import("katex/contrib/auto-render")).default; + await import("katex/contrib/mhchem"); + await import("katex/dist/katex.min.css"); + + renderMathInElement(document.getElementById("content")); + document.body.classList.add("math-loaded"); +} diff --git a/packages/share-theme/src/styles/content.css b/packages/share-theme/src/styles/content.css index d500888a6..e67be725a 100644 --- a/packages/share-theme/src/styles/content.css +++ b/packages/share-theme/src/styles/content.css @@ -46,4 +46,8 @@ #content img { max-width: 100%; +} + +body:not(.math-loaded) .math-tex { + visibility: hidden; } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2755aca2..c8eab1bc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1329,6 +1329,9 @@ importers: packages/share-theme: dependencies: + katex: + specifier: 0.16.25 + version: 0.16.25 mermaid: specifier: 11.12.0 version: 11.12.0 @@ -15085,8 +15088,6 @@ snapshots: '@ckeditor/ckeditor5-core': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-code-block@47.1.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -15837,8 +15838,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-restricted-editing@47.1.0': dependencies: From 21b20cf5750d9e02c6801284a2d32978f09b58ba Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 21:18:06 +0300 Subject: [PATCH 040/190] chore(share): bring back most of the logic --- apps/client/src/share.ts | 48 ------------------- packages/share-theme/src/scripts/index.ts | 34 ++++++++++++- .../share-theme/src/scripts/modules/api.ts | 18 +++++++ 3 files changed, 50 insertions(+), 50 deletions(-) create mode 100644 packages/share-theme/src/scripts/modules/api.ts diff --git a/apps/client/src/share.ts b/apps/client/src/share.ts index 602f7e95c..579b9e422 100644 --- a/apps/client/src/share.ts +++ b/apps/client/src/share.ts @@ -9,8 +9,6 @@ async function ensureJQuery() { (window as any).$ = $; } - - async function formatCodeBlocks() { const anyCodeBlock = document.querySelector("#content pre"); if (!anyCodeBlock) { @@ -24,49 +22,3 @@ async function formatCodeBlocks() { async function setupTextNote() { formatCodeBlocks(); } - -/** - * Fetch note with given ID from backend - * - * @param noteId of the given note to be fetched. If false, fetches current note. - */ -async function fetchNote(noteId: string | null = null) { - if (!noteId) { - noteId = document.body.getAttribute("data-note-id"); - } - - const resp = await fetch(`api/notes/${noteId}`); - - return await resp.json(); -} - -document.addEventListener( - "DOMContentLoaded", - () => { - const noteType = determineNoteType(); - - if (noteType === "text") { - setupTextNote(); - } - - const toggleMenuButton = document.getElementById("toggleMenuButton"); - const layout = document.getElementById("layout"); - - if (toggleMenuButton && layout) { - toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu")); - } - }, - false -); - -function determineNoteType() { - const bodyClass = document.body.className; - const match = bodyClass.match(/type-([^\s]+)/); - return match ? match[1] : null; -} - -// workaround to prevent webpack from removing "fetchNote" as dead code: -// add fetchNote as property to the window object -Object.defineProperty(window, "fetchNote", { - value: fetchNote -}); diff --git a/packages/share-theme/src/scripts/index.ts b/packages/share-theme/src/scripts/index.ts index fbbc864d0..ea08330b0 100644 --- a/packages/share-theme/src/scripts/index.ts +++ b/packages/share-theme/src/scripts/index.ts @@ -5,6 +5,7 @@ import setupSearch from "./modules/search"; import setupThemeSelector from "./modules/theme"; import setupMermaid from "./modules/mermaid"; import setupMath from "./modules/math"; +import api from "./modules/api"; function $try unknown>(func: T, ...args: Parameters) { try { @@ -15,10 +16,39 @@ function $try unknown>(func: T, ...args: Paramete } } +Object.assign(window, api); $try(setupThemeSelector); $try(setupToC); $try(setupExpanders); $try(setupMobileMenu); $try(setupSearch); -$try(setupMermaid); -$try(setupMath); + +function setupTextNote() { + $try(setupMermaid); + $try(setupMath); +} + +document.addEventListener( + "DOMContentLoaded", + () => { + const noteType = determineNoteType(); + + if (noteType === "text") { + setupTextNote(); + } + + const toggleMenuButton = document.getElementById("toggleMenuButton"); + const layout = document.getElementById("layout"); + + if (toggleMenuButton && layout) { + toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu")); + } + }, + false +); + +function determineNoteType() { + const bodyClass = document.body.className; + const match = bodyClass.match(/type-([^\s]+)/); + return match ? match[1] : null; +} diff --git a/packages/share-theme/src/scripts/modules/api.ts b/packages/share-theme/src/scripts/modules/api.ts new file mode 100644 index 000000000..adaca77d1 --- /dev/null +++ b/packages/share-theme/src/scripts/modules/api.ts @@ -0,0 +1,18 @@ +/** + * Fetch note with given ID from backend + * + * @param noteId of the given note to be fetched. If false, fetches current note. + */ +async function fetchNote(noteId: string | null = null) { + if (!noteId) { + noteId = document.body.getAttribute("data-note-id"); + } + + const resp = await fetch(`api/notes/${noteId}`); + + return await resp.json(); +} + +export default { + fetchNote +}; From 395f33cd5b1fddba239f1398b12a81f0261f1a17 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 22:32:42 +0300 Subject: [PATCH 041/190] chore(share): bring back boxicons --- apps/server/src/share/content_renderer.ts | 1 - packages/share-theme/package.json | 3 ++- packages/share-theme/scripts/build.ts | 2 ++ packages/share-theme/src/scripts/index.ts | 1 + packages/share-theme/src/scripts/modules/math.ts | 3 ++- pnpm-lock.yaml | 7 +++++++ 6 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 3b5ce0a6d..953feef1c 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -100,7 +100,6 @@ export function renderNoteContent(note: SNote) { if (!note.isLabelTruthy("shareOmitDefaultCss")) { cssToLoad.push(`assets/styles.css`); cssToLoad.push(`assets/scripts.css`); - cssToLoad.push(`assets/boxicons.css`); } for (const cssRelation of note.getRelations("shareCss")) { cssToLoad.push(`api/notes/${cssRelation.value}/download`); diff --git a/packages/share-theme/package.json b/packages/share-theme/package.json index ecaacfb52..68916cc3e 100644 --- a/packages/share-theme/package.json +++ b/packages/share-theme/package.json @@ -23,7 +23,8 @@ "license": "Apache-2.0", "dependencies": { "katex": "0.16.25", - "mermaid": "11.12.0" + "mermaid": "11.12.0", + "boxicons": "2.1.4" }, "devDependencies": { "@digitak/esrun": "3.2.26", diff --git a/packages/share-theme/scripts/build.ts b/packages/share-theme/scripts/build.ts index 073165f36..512935493 100644 --- a/packages/share-theme/scripts/build.ts +++ b/packages/share-theme/scripts/build.ts @@ -60,6 +60,8 @@ async function runBuild() { ".woff": "dataurl", ".woff2": "dataurl", ".ttf": "dataurl", + ".eot": "empty", + ".svg": "empty", ".html": "text", ".css": "css" }, diff --git a/packages/share-theme/src/scripts/index.ts b/packages/share-theme/src/scripts/index.ts index ea08330b0..f076a4bcc 100644 --- a/packages/share-theme/src/scripts/index.ts +++ b/packages/share-theme/src/scripts/index.ts @@ -6,6 +6,7 @@ import setupThemeSelector from "./modules/theme"; import setupMermaid from "./modules/mermaid"; import setupMath from "./modules/math"; import api from "./modules/api"; +import "boxicons/css/boxicons.min.css"; function $try unknown>(func: T, ...args: Parameters) { try { diff --git a/packages/share-theme/src/scripts/modules/math.ts b/packages/share-theme/src/scripts/modules/math.ts index a1c3195ac..bdba2ded9 100644 --- a/packages/share-theme/src/scripts/modules/math.ts +++ b/packages/share-theme/src/scripts/modules/math.ts @@ -1,3 +1,5 @@ +import "katex/dist/katex.min.css"; + export default async function setupMath() { const anyMathBlock = document.querySelector("#content .math-tex"); if (!anyMathBlock) { @@ -6,7 +8,6 @@ export default async function setupMath() { const renderMathInElement = (await import("katex/contrib/auto-render")).default; await import("katex/contrib/mhchem"); - await import("katex/dist/katex.min.css"); renderMathInElement(document.getElementById("content")); document.body.classList.add("math-loaded"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8eab1bc0..d5cd6af3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1329,6 +1329,9 @@ importers: packages/share-theme: dependencies: + boxicons: + specifier: 2.1.4 + version: 2.1.4 katex: specifier: 0.16.25 version: 0.16.25 @@ -15088,6 +15091,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@47.1.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -15838,6 +15843,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-restricted-editing@47.1.0': dependencies: From 0c399a676a0ea86de6bb8f905d8cb8d8aa071940 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 24 Oct 2025 23:28:42 +0300 Subject: [PATCH 042/190] chore(share): fix typecheck issue --- packages/share-theme/src/types.d.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/share-theme/src/types.d.ts diff --git a/packages/share-theme/src/types.d.ts b/packages/share-theme/src/types.d.ts new file mode 100644 index 000000000..3fa19bd49 --- /dev/null +++ b/packages/share-theme/src/types.d.ts @@ -0,0 +1,5 @@ +declare module "katex/contrib/auto-render" { + export default function renderMathInElement(elem: HTMLElement, options?: {}) +} + +declare module "katex/contrib/mhchem" {} From 1182592fc58e6ba2d5375262cb95920e03fdcfaf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 26 Oct 2025 10:07:42 +0200 Subject: [PATCH 043/190] chore(share): fix another typecheck issue --- packages/share-theme/src/scripts/modules/math.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/share-theme/src/scripts/modules/math.ts b/packages/share-theme/src/scripts/modules/math.ts index bdba2ded9..3893dbeed 100644 --- a/packages/share-theme/src/scripts/modules/math.ts +++ b/packages/share-theme/src/scripts/modules/math.ts @@ -9,6 +9,8 @@ export default async function setupMath() { const renderMathInElement = (await import("katex/contrib/auto-render")).default; await import("katex/contrib/mhchem"); - renderMathInElement(document.getElementById("content")); + const contentEl = document.getElementById("content"); + if (!contentEl) return; + renderMathInElement(contentEl); document.body.classList.add("math-loaded"); } From 212956201a3c1dd56cb4a9b15835104b1df61917 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 26 Oct 2025 11:02:19 +0200 Subject: [PATCH 044/190] chore(export/share): export full share script & styles --- apps/server/src/routes/assets.ts | 13 +++++++++++-- apps/server/src/services/export/zip/share_theme.ts | 12 +++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/server/src/routes/assets.ts b/apps/server/src/routes/assets.ts index efcb5a335..1e2ff30b1 100644 --- a/apps/server/src/routes/assets.ts +++ b/apps/server/src/routes/assets.ts @@ -32,7 +32,6 @@ async function register(app: express.Application) { req.url = `/${assetUrlFragment}` + req.url; vite.middlewares(req, res, next); }); - app.use(`/share/assets/`, express.static(path.join(srcRoot, "../../packages/share-theme/dist"))); } else { const publicDir = path.join(resourceDir, "public"); if (!existsSync(publicDir)) { @@ -43,9 +42,9 @@ async function register(app: express.Application) { app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(publicDir, "stylesheets"))); app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(publicDir, "fonts"))); app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(publicDir, "translations"))); - app.use(`/share/assets/`, persistentCacheStatic(path.join(resourceDir, "share-theme/assets"))); app.use(`/node_modules/`, persistentCacheStatic(path.join(publicDir, "node_modules"))); } + app.use(`/share/assets/`, express.static(getShareThemeAssetDir())); app.use(`/${assetUrlFragment}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images"))); app.use(`/${assetUrlFragment}/doc_notes`, persistentCacheStatic(path.join(resourceDir, "assets", "doc_notes"))); app.use(`/assets/vX/fonts`, express.static(path.join(srcRoot, "public/fonts"))); @@ -53,6 +52,16 @@ async function register(app: express.Application) { app.use(`/assets/vX/stylesheets`, express.static(path.join(srcRoot, "public/stylesheets"))); } +export function getShareThemeAssetDir() { + if (process.env.NODE_ENV === "development") { + const srcRoot = path.join(__dirname, "..", ".."); + return path.join(srcRoot, "../../packages/share-theme/dist"); + } else { + const resourceDir = getResourceDir(); + return path.join(resourceDir, "share-theme/assets"); + } +} + export default { register }; diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 06609b031..3439fe3e4 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -3,10 +3,13 @@ import NoteMeta, { NoteMetaFile } from "../../meta/note_meta"; import { ExportFormat, ZipExportProvider } from "./abstract_provider.js"; import { RESOURCE_DIR } from "../../resource_dir"; import { getResourceDir, isDev } from "../../utils"; -import fs from "fs"; +import fs, { readdirSync } from "fs"; import { renderNoteForExport } from "../../../share/content_renderer"; import type BNote from "../../../becca/entities/bnote.js"; import type BBranch from "../../../becca/entities/bbranch.js"; +import { getShareThemeAssetDir } from "../../../routes/assets"; + +const shareThemeAssetDir = getShareThemeAssetDir(); export default class ShareThemeExportProvider extends ZipExportProvider { @@ -14,6 +17,7 @@ export default class ShareThemeExportProvider extends ZipExportProvider { private indexMeta: NoteMeta | null = null; prepareMeta(metaFile: NoteMetaFile): void { + const assets = [ "style.css", "script.js", @@ -26,6 +30,10 @@ export default class ShareThemeExportProvider extends ZipExportProvider { "icon-color.svg" ]; + for (const file of readdirSync(shareThemeAssetDir)) { + assets.push(`assets/${file}`); + } + for (const asset of assets) { const assetMeta = { noImport: true, @@ -107,6 +115,8 @@ function getShareThemeAssets(nameWithExtension: string) { let path: string | undefined; if (nameWithExtension === "icon-color.svg") { path = join(RESOURCE_DIR, "images", nameWithExtension); + } else if (nameWithExtension.startsWith("assets")) { + path = join(shareThemeAssetDir, nameWithExtension.replace(/^assets\//, "")); } else if (isDev) { path = join(getResourceDir(), "..", "..", "client", "dist", "src", nameWithExtension); } else { From f4468706ef56f7d5affb799d847731ceb8a07b4d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 26 Oct 2025 11:07:08 +0200 Subject: [PATCH 045/190] fix(export/share): asset path for styles and scripts --- apps/server/src/services/export/zip/share_theme.ts | 2 -- apps/server/src/share/content_renderer.ts | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 3439fe3e4..cb04b847a 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -19,8 +19,6 @@ export default class ShareThemeExportProvider extends ZipExportProvider { prepareMeta(metaFile: NoteMetaFile): void { const assets = [ - "style.css", - "script.js", "boxicons.css", "boxicons.eot", "boxicons.woff2", diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 953feef1c..73234a79f 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -70,11 +70,10 @@ export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath subRoot, rootNoteId: parentBranch.noteId, cssToLoad: [ - `${basePath}style.css`, - `${basePath}boxicons.css` + `${basePath}assets/styles.css`, ], jsToLoad: [ - `${basePath}script.js` + `${basePath}assets/scripts.js` ], logoUrl: `${basePath}icon-color.svg`, ancestors From 055fcb7b2a8072181b480e8ebef42f30ce02c513 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 26 Oct 2025 11:34:09 +0200 Subject: [PATCH 046/190] fix(export/share): handling of fonts --- apps/server/src/services/export/zip/share_theme.ts | 6 ------ apps/server/src/share/content_renderer.ts | 1 + packages/share-theme/scripts/build.ts | 6 +++--- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index cb04b847a..c33951c03 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -19,12 +19,6 @@ export default class ShareThemeExportProvider extends ZipExportProvider { prepareMeta(metaFile: NoteMetaFile): void { const assets = [ - "boxicons.css", - "boxicons.eot", - "boxicons.woff2", - "boxicons.woff", - "boxicons.ttf", - "boxicons.svg", "icon-color.svg" ]; diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 73234a79f..4acde9b86 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -71,6 +71,7 @@ export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath rootNoteId: parentBranch.noteId, cssToLoad: [ `${basePath}assets/styles.css`, + `${basePath}assets/scripts.css`, ], jsToLoad: [ `${basePath}assets/scripts.js` diff --git a/packages/share-theme/scripts/build.ts b/packages/share-theme/scripts/build.ts index 512935493..13b2ac493 100644 --- a/packages/share-theme/scripts/build.ts +++ b/packages/share-theme/scripts/build.ts @@ -57,9 +57,9 @@ async function runBuild() { loader: { ".png": "dataurl", ".gif": "dataurl", - ".woff": "dataurl", - ".woff2": "dataurl", - ".ttf": "dataurl", + ".woff": "file", + ".woff2": "file", + ".ttf": "file", ".eot": "empty", ".svg": "empty", ".html": "text", From ba26c478d6aa68f0107592ce7a847864a239b029 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 26 Oct 2025 12:15:35 +0200 Subject: [PATCH 047/190] fix(export/share): assets incorrectly rewritten --- apps/server/src/services/export/zip/share_theme.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index c33951c03..e33390735 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -52,7 +52,10 @@ export default class ShareThemeExportProvider extends ZipExportProvider { if (note) { content = renderNoteForExport(note, branch, basePath, noteMeta.notePath.slice(0, -1)); if (typeof content === "string") { - content = content.replace(/href="[^"]*\.\/([a-zA-Z0-9_\/]{12})[^"]*"/g, "href=\"#root/$1\""); + content = content.replace(/href="[^"]*\.\/([a-zA-Z0-9_\/]{12})[^"]*"/g, (match, id) => { + if (match.includes("/assets/")) return match; + return `href="#root/${id}"`; + }); content = this.rewriteFn(content, noteMeta); } } From 52a6f2597e4a74b9916d8e17dad494525fe16f6b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 26 Oct 2025 21:38:16 +0200 Subject: [PATCH 048/190] fix(share): template directory in production --- apps/server/src/share/content_renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 4acde9b86..611930f04 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -9,7 +9,7 @@ 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"; -import utils, { isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; +import utils, { getResourceDir, isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; import ejs from "ejs"; import log from "../services/log.js"; import { join } from "path"; @@ -199,7 +199,7 @@ function getDefaultTemplatePath(template: string) { // Path is relative to apps/server/dist/assets/views return process.env.NODE_ENV === "development" ? join(__dirname, `../../../../packages/share-theme/src/templates/${template}.ejs`) - : `../../share-theme/templates/${template}.ejs`; + : join(getResourceDir(), `share-theme/templates/${template}.ejs`); } function readTemplate(path: string) { From 3661733f070cda154d7d96da4d58b4260d93bfb1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 26 Oct 2025 22:00:11 +0200 Subject: [PATCH 049/190] chore(server): remove duplicate math handling --- apps/server/src/share/content_renderer.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 611930f04..2fd6d55a9 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -301,19 +301,6 @@ function renderText(result: Result, note: SNote | BNote) { result.content = document.innerHTML ?? ""; - if (result.content.includes(``)) { - result.header += ` - - - - -`; - } - if (note.hasLabel("shareIndex")) { renderIndex(result); } From 21d243eec1ca5794bf66f77bdbd6c709155abbc7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 27 Oct 2025 20:29:56 +0200 Subject: [PATCH 050/190] fix(desktop): share not working --- apps/desktop/scripts/build.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/scripts/build.ts b/apps/desktop/scripts/build.ts index 945a4cfeb..0b76d8b25 100644 --- a/apps/desktop/scripts/build.ts +++ b/apps/desktop/scripts/build.ts @@ -11,6 +11,7 @@ async function main() { // Copy assets. build.copy("src/assets", "assets/"); build.copy("/apps/server/src/assets", "assets/"); + build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/"); build.copy("/packages/share-theme/src/templates", "share-theme/templates/"); // Copy node modules dependencies From 12df6a0d6eddfed3e17783b5e266010bdf6e0e95 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 27 Oct 2025 20:53:13 +0200 Subject: [PATCH 051/190] chore(uikit): create empty project --- packages/uikit/package.json | 18 ++++++++++++++++++ pnpm-lock.yaml | 18 ++++++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 packages/uikit/package.json diff --git a/packages/uikit/package.json b/packages/uikit/package.json new file mode 100644 index 000000000..484ea7ca9 --- /dev/null +++ b/packages/uikit/package.json @@ -0,0 +1,18 @@ +{ + "name": "@triliumnext/uikit", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@triliumnext/commons": "workspace:*", + "@triliumnext/highlightjs": "workspace:*", + "jquery": "3.7.1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.18.3" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5cd6af3d..e5f89b028 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1394,6 +1394,18 @@ importers: specifier: 0.0.3 version: 0.0.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) + packages/uikit: + dependencies: + '@triliumnext/commons': + specifier: workspace:* + version: link:../commons + '@triliumnext/highlightjs': + specifier: workspace:* + version: link:../highlightjs + jquery: + specifier: 3.7.1 + version: 3.7.1 + packages: '@ampproject/remapping@2.3.0': @@ -15091,8 +15103,6 @@ snapshots: '@ckeditor/ckeditor5-core': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-code-block@47.1.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -15345,8 +15355,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.1.0': dependencies: @@ -15369,8 +15377,6 @@ snapshots: '@ckeditor/ckeditor5-table': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-emoji@47.1.0': dependencies: From 14b8d0a47ef0ca0ef25220d10159cd6da1485c86 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 27 Oct 2025 22:18:08 +0200 Subject: [PATCH 052/190] chore(share): bring back syntax highlight --- apps/server/package.json | 1 + apps/server/src/share/content_renderer.ts | 18 +++++++++++++++--- packages/share-theme/src/scripts/index.ts | 1 + pnpm-lock.yaml | 9 +++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index 5cc30b1c9..7f59fe280 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -36,6 +36,7 @@ "@triliumnext/commons": "workspace:*", "@triliumnext/express-partial-content": "workspace:*", "@triliumnext/turndown-plugin-gfm": "workspace:*", + "@triliumnext/highlightjs": "workspace:*", "@types/archiver": "6.0.3", "@types/better-sqlite3": "7.6.13", "@types/cls-hooked": "4.3.9", diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 2fd6d55a9..3ab219f26 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -1,4 +1,4 @@ -import { parse, HTMLElement, TextNode } from "node-html-parser"; +import { parse, HTMLElement, TextNode, Options } from "node-html-parser"; import shaca from "./shaca/shaca.js"; import assetPath, { assetUrlFragment } from "../services/asset_path.js"; import shareRoot from "./share_root.js"; @@ -14,6 +14,7 @@ import ejs from "ejs"; import log from "../services/log.js"; import { join } from "path"; import { readFileSync } from "fs"; +import { highlightAuto } from "@triliumnext/highlightjs"; const shareAdjustedAssetPath = isDev ? assetPath : `../${assetPath}`; const templateCache: Map = new Map(); @@ -264,7 +265,10 @@ function renderIndex(result: Result) { function renderText(result: Result, note: SNote | BNote) { if (typeof result.content !== "string") return; - const document = parse(result.content || ""); + const parseOpts: Partial = { + blockTextElements: {} + } + const document = parse(result.content || "", parseOpts); // Process include notes. for (const includeNoteEl of document.querySelectorAll("section.include-note")) { @@ -277,7 +281,7 @@ function renderText(result: Result, note: SNote | BNote) { const includedResult = getContent(note); if (typeof includedResult.content !== "string") continue; - const includedDocument = parse(includedResult.content).childNodes; + const includedDocument = parse(includedResult.content, parseOpts).childNodes; if (includedDocument) { includeNoteEl.replaceWith(...includedDocument); } @@ -286,6 +290,7 @@ function renderText(result: Result, note: SNote | BNote) { result.isEmpty = document.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0; if (!result.isEmpty) { + // Process attachment links. for (const linkEl of document.querySelectorAll("a")) { const href = linkEl.getAttribute("href"); @@ -299,6 +304,13 @@ function renderText(result: Result, note: SNote | BNote) { } } + // Apply syntax highlight. + for (const codeEl of document.querySelectorAll("pre code")) { + const highlightResult = highlightAuto(codeEl.innerText); + codeEl.innerHTML = highlightResult.value; + codeEl.classList.add("hljs"); + } + result.content = document.innerHTML ?? ""; if (note.hasLabel("shareIndex")) { diff --git a/packages/share-theme/src/scripts/index.ts b/packages/share-theme/src/scripts/index.ts index f076a4bcc..a809962aa 100644 --- a/packages/share-theme/src/scripts/index.ts +++ b/packages/share-theme/src/scripts/index.ts @@ -7,6 +7,7 @@ import setupMermaid from "./modules/mermaid"; import setupMath from "./modules/math"; import api from "./modules/api"; import "boxicons/css/boxicons.min.css"; +import "highlight.js/styles/default.css"; function $try unknown>(func: T, ...args: Parameters) { try { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5f89b028..8b3ab73d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,6 +480,9 @@ importers: '@triliumnext/express-partial-content': specifier: workspace:* version: link:../../packages/express-partial-content + '@triliumnext/highlightjs': + specifier: workspace:* + version: link:../../packages/highlightjs '@triliumnext/turndown-plugin-gfm': specifier: workspace:* version: link:../../packages/turndown-plugin-gfm @@ -15103,6 +15106,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@47.1.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -15355,6 +15360,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.1.0': dependencies: @@ -15377,6 +15384,8 @@ snapshots: '@ckeditor/ckeditor5-table': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-emoji@47.1.0': dependencies: From 3bb224e68250228c9d886446a7ae683d72a5f539 Mon Sep 17 00:00:00 2001 From: migraine-user Date: Mon, 27 Oct 2025 20:23:10 +0100 Subject: [PATCH 053/190] Translated using Weblate (Korean) Currently translated at 1.3% (2 of 152 strings) Translation: Trilium Notes/Website Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/ --- apps/website/src/translations/ko/translation.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/website/src/translations/ko/translation.json b/apps/website/src/translations/ko/translation.json index 0967ef424..364c1b188 100644 --- a/apps/website/src/translations/ko/translation.json +++ b/apps/website/src/translations/ko/translation.json @@ -1 +1,6 @@ -{} +{ + "hero_section": { + "github": "깃허브", + "dockerhub": "도커 허브" + } +} From c2c8417c420cf6508bcdf0bb714a66aac5b09eb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:49:55 +0000 Subject: [PATCH 054/190] chore(deps): update dependency @types/express to v5.0.5 --- _regroup/package.json | 2 +- package.json | 2 +- pnpm-lock.yaml | 20 +++++++++----------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/_regroup/package.json b/_regroup/package.json index 1937327e0..721fe4390 100644 --- a/_regroup/package.json +++ b/_regroup/package.json @@ -37,7 +37,7 @@ "devDependencies": { "@playwright/test": "1.56.1", "@stylistic/eslint-plugin": "5.5.0", - "@types/express": "5.0.4", + "@types/express": "5.0.5", "@types/node": "22.18.12", "@types/yargs": "17.0.34", "@vitest/coverage-v8": "3.2.4", diff --git a/package.json b/package.json index aa44a8bf0..a59bc6c0b 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@fast-csv/parse": "5.0.5", "@playwright/test": "1.56.1", "@triliumnext/server": "workspace:*", - "@types/express": "5.0.4", + "@types/express": "5.0.5", "@types/node": "22.18.12", "@vitest/coverage-v8": "3.2.4", "@vitest/ui": "3.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6836ee417..b562fb909 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,8 +50,8 @@ importers: specifier: workspace:* version: link:apps/server '@types/express': - specifier: 5.0.4 - version: 5.0.4 + specifier: 5.0.5 + version: 5.0.5 '@types/node': specifier: 22.18.12 version: 22.18.12 @@ -497,7 +497,7 @@ importers: version: 1.8.1 '@types/cookie-parser': specifier: 1.4.10 - version: 1.4.10(@types/express@5.0.4) + version: 1.4.10(@types/express@5.0.5) '@types/debounce': specifier: 1.2.4 version: 1.2.4 @@ -4958,8 +4958,8 @@ packages: '@types/express@5.0.3': resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} - '@types/express@5.0.4': - resolution: {integrity: sha512-g64dbryHk7loCIrsa0R3shBnEu5p6LPJ09bu9NG58+jz+cRUjFrc3Bz0kNQ7j9bXeCsrRDvNET1G54P/GJkAyA==} + '@types/express@5.0.5': + resolution: {integrity: sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==} '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} @@ -15280,8 +15280,6 @@ snapshots: '@ckeditor/ckeditor5-table': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-emoji@47.1.0': dependencies: @@ -19310,9 +19308,9 @@ snapshots: dependencies: '@types/node': 22.18.12 - '@types/cookie-parser@1.4.10(@types/express@5.0.4)': + '@types/cookie-parser@1.4.10(@types/express@5.0.5)': dependencies: - '@types/express': 5.0.4 + '@types/express': 5.0.5 '@types/cookie@0.6.0': optional: true @@ -19502,7 +19500,7 @@ snapshots: '@types/express-serve-static-core': 5.1.0 '@types/serve-static': 1.15.10 - '@types/express@5.0.4': + '@types/express@5.0.5': dependencies: '@types/body-parser': 1.19.6 '@types/express-serve-static-core': 5.1.0 @@ -19703,7 +19701,7 @@ snapshots: '@types/serve-index@1.9.4': dependencies: - '@types/express': 5.0.4 + '@types/express': 5.0.5 '@types/serve-static@1.15.10': dependencies: From 927cd0255ef03d766dc7e172ef22280582a39c70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:50:45 +0000 Subject: [PATCH 055/190] fix(deps): update dependency mermaid to v11.12.1 --- apps/client/package.json | 2 +- package.json | 2 +- pnpm-lock.yaml | 42 ++++++++++++++++++++-------------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index 61738b8bb..a4e2e1c5a 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -54,7 +54,7 @@ "leaflet-gpx": "2.2.0", "mark.js": "8.11.1", "marked": "16.4.1", - "mermaid": "11.12.0", + "mermaid": "11.12.1", "mind-elixir": "5.3.4", "normalize.css": "8.0.1", "panzoom": "9.4.3", diff --git a/package.json b/package.json index aa44a8bf0..7c8a4ce9d 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "ckeditor5": "patches/ckeditor5.patch" }, "overrides": { - "mermaid": "11.12.0", + "mermaid": "11.12.1", "preact": "10.27.2", "roughjs": "4.6.6", "@types/express-serve-static-core": "5.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6836ee417..448621d4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - mermaid: 11.12.0 + mermaid: 11.12.1 preact: 10.27.2 roughjs: 4.6.6 '@types/express-serve-static-core': 5.1.0 @@ -156,7 +156,7 @@ importers: version: 0.1.3(@types/leaflet@1.9.21)(leaflet@1.9.4)(maplibre-gl@5.6.1) '@mermaid-js/layout-elk': specifier: 0.2.0 - version: 0.2.0(mermaid@11.12.0) + version: 0.2.0(mermaid@11.12.1) '@mind-elixir/node-menu': specifier: 5.0.0 version: 5.0.0(mind-elixir@5.3.4) @@ -245,8 +245,8 @@ importers: specifier: 16.4.1 version: 16.4.1 mermaid: - specifier: 11.12.0 - version: 11.12.0 + specifier: 11.12.1 + version: 11.12.1 mind-elixir: specifier: 5.3.4 version: 5.3.4 @@ -3349,10 +3349,10 @@ packages: '@mermaid-js/layout-elk@0.2.0': resolution: {integrity: sha512-vjjYGnCCjYlIA/rR7M//eFi0rHM6dsMyN1JQKfckpt30DTC/esrw36hcrvA2FNPHaqh3Q/SyBWzddyaky8EtUQ==} peerDependencies: - mermaid: 11.12.0 + mermaid: 11.12.1 - '@mermaid-js/parser@0.6.2': - resolution: {integrity: sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==} + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} '@microsoft/api-extractor-model@7.30.6': resolution: {integrity: sha512-znmFn69wf/AIrwHya3fxX6uB5etSIn6vg4Q4RB/tb5VDDs1rqREc+AvMC/p19MUN13CZ7+V/8pkYPTj7q8tftg==} @@ -6939,8 +6939,8 @@ packages: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} - dagre-d3-es@7.0.11: - resolution: {integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==} + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} @@ -9743,8 +9743,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.12.0: - resolution: {integrity: sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg==} + mermaid@11.12.1: + resolution: {integrity: sha512-UlIZrRariB11TY1RtTgUWp65tphtBv4CSq7vyS2ZZ2TgoMjs2nloq+wFqxiwcxlhHUvs7DPGgMjs2aeQxz5h9g==} methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} @@ -15004,6 +15004,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@47.1.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -15065,8 +15067,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 '@ckeditor/ckeditor5-watchdog': 47.1.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': dependencies: @@ -16928,7 +16928,7 @@ snapshots: '@excalidraw/mermaid-to-excalidraw@1.1.2': dependencies: '@excalidraw/markdown-to-text': 0.1.2 - mermaid: 11.12.0 + mermaid: 11.12.1 nanoid: 5.1.5 transitivePeerDependencies: - supports-color @@ -17748,13 +17748,13 @@ snapshots: '@mdn/browser-compat-data@5.7.6': {} - '@mermaid-js/layout-elk@0.2.0(mermaid@11.12.0)': + '@mermaid-js/layout-elk@0.2.0(mermaid@11.12.1)': dependencies: d3: 7.9.0 elkjs: 0.9.3 - mermaid: 11.12.0 + mermaid: 11.12.1 - '@mermaid-js/parser@0.6.2': + '@mermaid-js/parser@0.6.3': dependencies: langium: 3.3.1 @@ -22062,7 +22062,7 @@ snapshots: d3-transition: 3.0.1(d3-selection@3.0.0) d3-zoom: 3.0.0 - dagre-d3-es@7.0.11: + dagre-d3-es@7.0.13: dependencies: d3: 7.9.0 lodash-es: 4.17.21 @@ -25639,18 +25639,18 @@ snapshots: merge2@1.4.1: {} - mermaid@11.12.0: + mermaid@11.12.1: dependencies: '@braintree/sanitize-url': 7.1.1 '@iconify/utils': 3.0.1 - '@mermaid-js/parser': 0.6.2 + '@mermaid-js/parser': 0.6.3 '@types/d3': 7.4.3 cytoscape: 3.31.2 cytoscape-cose-bilkent: 4.1.0(cytoscape@3.31.2) cytoscape-fcose: 2.2.0(cytoscape@3.31.2) d3: 7.9.0 d3-sankey: 0.12.3 - dagre-d3-es: 7.0.11 + dagre-d3-es: 7.0.13 dayjs: 1.11.18 dompurify: 3.2.5 katex: 0.16.25 From 96c949b2fc491043ad09abb9d44be31faa746190 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:51:32 +0000 Subject: [PATCH 056/190] fix(deps): update dependency react-i18next to v16.2.1 --- apps/client/package.json | 2 +- apps/website/package.json | 2 +- pnpm-lock.yaml | 31 ++++++++++++------------------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index 61738b8bb..cd5bc1ebd 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -59,7 +59,7 @@ "normalize.css": "8.0.1", "panzoom": "9.4.3", "preact": "10.27.2", - "react-i18next": "16.2.0", + "react-i18next": "16.2.1", "reveal.js": "5.2.1", "svg-pan-zoom": "3.6.2", "tabulator-tables": "6.3.1", diff --git a/apps/website/package.json b/apps/website/package.json index 4dcb83c00..cf27abca5 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -14,7 +14,7 @@ "preact": "10.27.2", "preact-iso": "2.11.0", "preact-render-to-string": "6.6.3", - "react-i18next": "16.2.0" + "react-i18next": "16.2.1" }, "devDependencies": { "@preact/preset-vite": "2.10.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6836ee417..0f7c8e100 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,8 +260,8 @@ importers: specifier: 10.27.2 version: 10.27.2 react-i18next: - specifier: 16.2.0 - version: 16.2.0(i18next@25.6.0(typescript@5.9.3))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.3) + specifier: 16.2.1 + version: 16.2.1(i18next@25.6.0(typescript@5.9.3))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.3) reveal.js: specifier: 5.2.1 version: 5.2.1 @@ -790,8 +790,8 @@ importers: specifier: 6.6.3 version: 6.6.3(preact@10.27.2) react-i18next: - specifier: 16.2.0 - version: 16.2.0(i18next@25.6.0(typescript@5.9.3))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.3) + specifier: 16.2.1 + version: 16.2.1(i18next@25.6.0(typescript@5.9.3))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.3) devDependencies: '@preact/preset-vite': specifier: 2.10.2 @@ -11734,8 +11734,8 @@ packages: peerDependencies: react: ^19.1.0 - react-i18next@16.2.0: - resolution: {integrity: sha512-giCEDa6NtQYvuLGW9xaBo4HCVxT0Y8jrOpX/uSnd+lZ3Dmm/2BNrGNEgRZzWDkSobmE6IWPJ+/Ow9Rhi/gP6+g==} + react-i18next@16.2.1: + resolution: {integrity: sha512-z7TVwd8q4AjFo2n7oOwzNusY7xVL4uHykwX1zZRvasUQnmnXlp7Z1FZqXvhK/6hQaCvWTZmZW1bMaUWKowtvVw==} peerDependencies: i18next: '>= 25.5.2' react: '>= 16.8.0' @@ -13521,11 +13521,6 @@ packages: '@types/react': optional: true - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -15004,6 +14999,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@47.1.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -15065,8 +15062,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 '@ckeditor/ckeditor5-watchdog': 47.1.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': dependencies: @@ -15258,6 +15253,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.1.0': dependencies: @@ -27830,7 +27827,7 @@ snapshots: react: 16.14.0 scheduler: 0.26.0 - react-i18next@16.2.0(i18next@25.6.0(typescript@5.9.3))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.3): + react-i18next@16.2.1(i18next@25.6.0(typescript@5.9.3))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 @@ -30084,10 +30081,6 @@ snapshots: optionalDependencies: '@types/react': 19.1.7 - use-sync-external-store@1.5.0(react@16.14.0): - dependencies: - react: 16.14.0 - use-sync-external-store@1.6.0(react@16.14.0): dependencies: react: 16.14.0 @@ -30898,7 +30891,7 @@ snapshots: zustand@4.5.6(@types/react@19.1.7)(react@16.14.0): dependencies: - use-sync-external-store: 1.5.0(react@16.14.0) + use-sync-external-store: 1.6.0(react@16.14.0) optionalDependencies: '@types/react': 19.1.7 react: 16.14.0 From 27d1044ba8a365a4076ef297d7a9bd44e00201b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:52:22 +0000 Subject: [PATCH 057/190] chore(deps): update dependency axios to v1.13.0 --- apps/server/package.json | 2 +- pnpm-lock.yaml | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index 1380183ad..83dd2614f 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -66,7 +66,7 @@ "@types/xml2js": "0.4.14", "archiver": "7.0.1", "async-mutex": "0.5.0", - "axios": "1.12.2", + "axios": "1.13.0", "bindings": "1.5.0", "bootstrap": "5.3.8", "chardet": "2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6836ee417..b2c952302 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -574,8 +574,8 @@ importers: specifier: 0.5.0 version: 0.5.0 axios: - specifier: 1.12.2 - version: 1.12.2(debug@4.4.3) + specifier: 1.13.0 + version: 1.13.0(debug@4.4.3) bindings: specifier: 1.5.0 version: 1.5.0 @@ -5830,8 +5830,8 @@ packages: resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} engines: {node: '>=6.0.0'} - axios@1.12.2: - resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + axios@1.13.0: + resolution: {integrity: sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==} b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} @@ -15065,8 +15065,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 '@ckeditor/ckeditor5-watchdog': 47.1.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': dependencies: @@ -20577,7 +20575,7 @@ snapshots: await-to-js@3.0.0: {} - axios@1.12.2(debug@4.4.3): + axios@1.13.0(debug@4.4.3): dependencies: follow-redirects: 1.15.9(debug@4.4.3) form-data: 4.0.4 From ac7e4580f6514c7eefd38837557dc3963a0c5ad4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:53:07 +0000 Subject: [PATCH 058/190] chore(deps): update dependency @types/archiver to v7 --- apps/server/package.json | 2 +- pnpm-lock.yaml | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index 1380183ad..63b392a2f 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -36,7 +36,7 @@ "@triliumnext/commons": "workspace:*", "@triliumnext/express-partial-content": "workspace:*", "@triliumnext/turndown-plugin-gfm": "workspace:*", - "@types/archiver": "6.0.4", + "@types/archiver": "7.0.0", "@types/better-sqlite3": "7.6.13", "@types/cls-hooked": "4.3.9", "@types/compression": "1.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6836ee417..7874b9728 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -484,8 +484,8 @@ importers: specifier: workspace:* version: link:../../packages/turndown-plugin-gfm '@types/archiver': - specifier: 6.0.4 - version: 6.0.4 + specifier: 7.0.0 + version: 7.0.0 '@types/better-sqlite3': specifier: 7.6.13 version: 7.6.13 @@ -4754,8 +4754,8 @@ packages: '@types/appdmg@0.5.5': resolution: {integrity: sha512-G+n6DgZTZFOteITE30LnWj+HRVIGr7wMlAiLWOO02uJFWVEitaPU9JVXm9wJokkgshBawb2O1OykdcsmkkZfgg==} - '@types/archiver@6.0.4': - resolution: {integrity: sha512-ULdQpARQ3sz9WH4nb98mJDYA0ft2A8C4f4fovvUcFwINa1cgGjY36JCAYuP5YypRq4mco1lJp1/7jEMS2oR0Hg==} + '@types/archiver@7.0.0': + resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==} '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -15065,8 +15065,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 '@ckeditor/ckeditor5-watchdog': 47.1.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': dependencies: @@ -19246,7 +19244,7 @@ snapshots: '@types/node': 22.18.12 optional: true - '@types/archiver@6.0.4': + '@types/archiver@7.0.0': dependencies: '@types/readdir-glob': 1.1.5 From b59fab9dba959c9c8366fcb4faf7d18fdf452143 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:53:53 +0000 Subject: [PATCH 059/190] chore(deps): update dependency @types/serve-static to v2 --- apps/server/package.json | 2 +- pnpm-lock.yaml | 28 ++++++++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index 1380183ad..fd2c8ae4f 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -56,7 +56,7 @@ "@types/sanitize-html": "2.16.0", "@types/sax": "1.2.7", "@types/serve-favicon": "2.5.7", - "@types/serve-static": "1.15.10", + "@types/serve-static": "2.2.0", "@types/stream-throttle": "0.1.4", "@types/supertest": "6.0.3", "@types/swagger-ui-express": "4.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6836ee417..b913e0b42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -544,8 +544,8 @@ importers: specifier: 2.5.7 version: 2.5.7 '@types/serve-static': - specifier: 1.15.10 - version: 1.15.10 + specifier: 2.2.0 + version: 2.2.0 '@types/stream-throttle': specifier: 0.1.4 version: 0.1.4 @@ -5143,6 +5143,9 @@ packages: '@types/serve-static@1.15.10': resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/sinonjs__fake-timers@8.1.5': resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} @@ -14858,6 +14861,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.1.0 '@ckeditor/ckeditor5-upload': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-ai@47.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: @@ -15004,6 +15009,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.1.0 '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@47.1.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -15065,8 +15072,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 '@ckeditor/ckeditor5-watchdog': 47.1.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': dependencies: @@ -15258,6 +15263,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.1.0 ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.1.0': dependencies: @@ -19494,19 +19501,19 @@ snapshots: '@types/body-parser': 1.19.6 '@types/express-serve-static-core': 5.1.0 '@types/qs': 6.14.0 - '@types/serve-static': 1.15.10 + '@types/serve-static': 2.2.0 '@types/express@5.0.3': dependencies: '@types/body-parser': 1.19.6 '@types/express-serve-static-core': 5.1.0 - '@types/serve-static': 1.15.10 + '@types/serve-static': 2.2.0 '@types/express@5.0.4': dependencies: '@types/body-parser': 1.19.6 '@types/express-serve-static-core': 5.1.0 - '@types/serve-static': 1.15.10 + '@types/serve-static': 2.2.0 '@types/fs-extra@11.0.4': dependencies: @@ -19711,6 +19718,11 @@ snapshots: '@types/node': 22.18.12 '@types/send': 0.17.5 + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.18.12 + '@types/sinonjs__fake-timers@8.1.5': {} '@types/sizzle@2.3.9': {} @@ -19745,7 +19757,7 @@ snapshots: '@types/swagger-ui-express@4.1.8': dependencies: '@types/express': 5.0.3 - '@types/serve-static': 1.15.10 + '@types/serve-static': 2.2.0 '@types/swagger-ui@5.21.1': {} From 507910b0ce4be20c75c4405a2250369fe63fceee Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Tue, 28 Oct 2025 10:17:37 +0800 Subject: [PATCH 060/190] fix: import markdown to text note --- apps/client/src/components/app_context.ts | 3 +- apps/client/src/services/glob.ts | 3 -- apps/client/src/types.d.ts | 1 - .../src/widgets/dialogs/markdown_import.tsx | 38 +++++++------------ .../src/widgets/type_widgets/editable_text.ts | 28 ++++++++++++++ packages/ckeditor5/src/augmentation.ts | 1 - .../ckeditor5/src/plugins/markdownimport.ts | 5 ++- 7 files changed, 47 insertions(+), 32 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index ce33d1447..f52f2e498 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -218,7 +218,6 @@ export type CommandMappings = { /** Works only in the electron context menu. */ replaceMisspelling: CommandData; - importMarkdownInline: CommandData; showPasswordNotSet: CommandData; showProtectedSessionPasswordDialog: CommandData; showUploadAttachmentsDialog: CommandData & { noteId: string }; @@ -306,7 +305,7 @@ export type CommandMappings = { addLinkToText: CommandData; followLinkUnderCursor: CommandData; insertDateTimeToText: CommandData; - pasteMarkdownIntoText: CommandData; + showMarkdownIntoTextDialog: CommandData & { textTypeWidget: EditableTextTypeWidget }; cutIntoNote: CommandData; addIncludeNoteToText: CommandData; editReadOnlyNote: CommandData; diff --git a/apps/client/src/services/glob.ts b/apps/client/src/services/glob.ts index 48d0d29a7..44ce64309 100644 --- a/apps/client/src/services/glob.ts +++ b/apps/client/src/services/glob.ts @@ -20,9 +20,6 @@ function setupGlobs() { window.glob.froca = froca; window.glob.treeCache = froca; // compatibility for CKEditor builds for a while - // for CKEditor integration (button on block toolbar) - window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline"); - window.onerror = function (msg, url, lineNo, columnNo, error) { const string = String(msg).toLowerCase(); diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index c5a93bd0a..d283983b4 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -26,7 +26,6 @@ interface CustomGlobals { appContext: AppContext; froca: Froca; treeCache: Froca; - importMarkdownInline: () => Promise; SEARCH_HELP_TEXT: string; activeDialog: JQuery | null; componentId: string; diff --git a/apps/client/src/widgets/dialogs/markdown_import.tsx b/apps/client/src/widgets/dialogs/markdown_import.tsx index d14d6fb11..5676d7298 100644 --- a/apps/client/src/widgets/dialogs/markdown_import.tsx +++ b/apps/client/src/widgets/dialogs/markdown_import.tsx @@ -7,6 +7,7 @@ import utils from "../../services/utils"; import Modal from "../react/Modal"; import Button from "../react/Button"; import { useTriliumEvent } from "../react/hooks"; +import EditableTextTypeWidget from "../type_widgets/editable_text"; interface RenderMarkdownResponse { htmlContent: string; @@ -14,29 +15,27 @@ interface RenderMarkdownResponse { export default function MarkdownImportDialog() { const markdownImportTextArea = useRef(null); + const [textTypeWidget, setTextTypeWidget] = useState(); const [ text, setText ] = useState(""); const [ shown, setShown ] = useState(false); - const triggerImport = useCallback(() => { - if (appContext.tabManager.getActiveContextNoteType() !== "text") { - return; - } - + useTriliumEvent("showMarkdownIntoTextDialog", ({ textTypeWidget }) => { + setTextTypeWidget(textTypeWidget); if (utils.isElectron()) { const { clipboard } = utils.dynamicRequire("electron"); const text = clipboard.readText(); - convertMarkdownToHtml(text); + convertMarkdownToHtml(text, textTypeWidget); } else { setShown(true); } - }, []); - - useTriliumEvent("importMarkdownInline", triggerImport); - useTriliumEvent("pasteMarkdownIntoText", triggerImport); + }); async function sendForm() { - await convertMarkdownToHtml(text); + if (textTypeWidget) { + await convertMarkdownToHtml(text, textTypeWidget); + } + setText(""); setShown(false); } @@ -44,7 +43,7 @@ export default function MarkdownImportDialog() { return ( } + footer={