diff --git a/apps/desktop/scripts/build.ts b/apps/desktop/scripts/build.ts index 679dc71f0..945a4cfeb 100644 --- a/apps/desktop/scripts/build.ts +++ b/apps/desktop/scripts/build.ts @@ -15,7 +15,6 @@ async function main() { // Copy node modules dependencies build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path", "@electron/remote" ]); - build.copy("/node_modules/jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js", "xhr-sync-worker.js"); build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css"); // Integrate the client. diff --git a/apps/server/package.json b/apps/server/package.json index b19a5945b..bae637d4c 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -25,7 +25,8 @@ "docker-start-rootless-alpine": "pnpm docker-build-rootless-alpine && docker run -p 8081:8080 triliumnext-rootless-alpine" }, "dependencies": { - "better-sqlite3": "12.4.1" + "better-sqlite3": "12.4.1", + "node-html-parser": "7.0.1" }, "devDependencies": { "@anthropic-ai/sdk": "0.64.0", @@ -49,7 +50,6 @@ "@types/html": "1.0.4", "@types/ini": "4.1.1", "@types/js-yaml": "4.0.9", - "@types/jsdom": "21.1.7", "@types/mime-types": "3.0.1", "@types/multer": "2.0.0", "@types/safe-compare": "1.1.2", @@ -105,7 +105,6 @@ "is-svg": "6.1.0", "jimp": "1.6.0", "js-yaml": "4.1.0", - "jsdom": "26.1.0", "marked": "16.3.0", "mime-types": "3.0.1", "multer": "2.0.2", diff --git a/apps/server/scripts/build.ts b/apps/server/scripts/build.ts index 84862d060..d2ed99ee2 100644 --- a/apps/server/scripts/build.ts +++ b/apps/server/scripts/build.ts @@ -11,7 +11,6 @@ async function main() { // Copy node modules dependencies build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path" ]); - build.copy("/node_modules/jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js", "xhr-sync-worker.js"); build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css"); // Integrate the client. diff --git a/apps/server/src/becca/similarity.ts b/apps/server/src/becca/similarity.ts index 11bfd93ed..a024fc455 100644 --- a/apps/server/src/becca/similarity.ts +++ b/apps/server/src/becca/similarity.ts @@ -2,7 +2,7 @@ import becca from "./becca.js"; import log from "../services/log.js"; import beccaService from "./becca_service.js"; import dateUtils from "../services/date_utils.js"; -import { JSDOM } from "jsdom"; +import { parse } from "node-html-parser"; import type BNote from "./entities/bnote.js"; import { SimilarNote } from "@triliumnext/commons"; @@ -123,10 +123,10 @@ export function buildRewardMap(note: BNote) { if (note.type === "text" && note.isDecrypted) { const content = note.getContent(); - const dom = new JSDOM(content); + const dom = parse(content.toString()); const addHeadingsToRewardMap = (elName: string, rewardFactor: number) => { - for (const el of dom.window.document.querySelectorAll(elName)) { + for (const el of dom.querySelectorAll(elName)) { addToRewardMap(el.textContent, rewardFactor); } }; diff --git a/apps/server/src/routes/api/clipper.ts b/apps/server/src/routes/api/clipper.ts index 0b87aabd6..7f6a39f9d 100644 --- a/apps/server/src/routes/api/clipper.ts +++ b/apps/server/src/routes/api/clipper.ts @@ -1,5 +1,5 @@ import type { Request } from "express"; -import jsdom from "jsdom"; +import { parse } from "node-html-parser"; import path from "path"; import type BNote from "../../becca/entities/bnote.js"; @@ -16,7 +16,6 @@ import log from "../../services/log.js"; import noteService from "../../services/notes.js"; import utils from "../../services/utils.js"; import ws from "../../services/ws.js"; -const { JSDOM } = jsdom; interface Image { src: string; @@ -181,10 +180,10 @@ export function processContent(images: Image[], note: BNote, content: string) { rewrittenContent = `

${rewrittenContent}

`; } // Create a JSDOM object from the existing HTML content - const dom = new JSDOM(rewrittenContent); + const dom = parse(rewrittenContent); // Get the content inside the body tag and serialize it - rewrittenContent = dom.window.document.body.innerHTML; + rewrittenContent = dom.querySelector("body")?.innerHTML ?? ""; return rewrittenContent; } diff --git a/apps/server/src/routes/api/note_map.ts b/apps/server/src/routes/api/note_map.ts index 616724960..6547ad6b9 100644 --- a/apps/server/src/routes/api/note_map.ts +++ b/apps/server/src/routes/api/note_map.ts @@ -1,10 +1,10 @@ "use strict"; import becca from "../../becca/becca.js"; -import { JSDOM } from "jsdom"; import type BNote from "../../becca/entities/bnote.js"; import type BAttribute from "../../becca/entities/battribute.js"; import type { Request } from "express"; +import { HTMLElement, parse, TextNode } from "node-html-parser"; import { BacklinkCountResponse, BacklinksResponse } from "@triliumnext/commons"; interface TreeLink { @@ -241,7 +241,7 @@ function updateDescendantCountMapForSearch(noteIdToDescendantCountMap: Record 0) { images[0]?.parentNode?.removeChild(images[0]); @@ -249,11 +249,11 @@ function removeImages(document: Document) { } const EXCERPT_CHAR_LIMIT = 200; -type ElementOrText = Element | Text; +type ElementOrText = HTMLElement | TextNode; export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { const html = sourceNote.getContent(); - const document = new JSDOM(html).window.document; + const document = parse(html.toString()); const excerpts: string[] = []; @@ -270,8 +270,8 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { let centerEl: HTMLElement = linkEl; - while (centerEl.tagName !== "BODY" && centerEl.parentElement && (centerEl.parentElement?.textContent?.length || 0) <= EXCERPT_CHAR_LIMIT) { - centerEl = centerEl.parentElement; + while (centerEl.tagName !== "BODY" && centerEl.parentNode && (centerEl.parentNode?.textContent?.length || 0) <= EXCERPT_CHAR_LIMIT) { + centerEl = centerEl.parentNode; } const excerptEls: ElementOrText[] = [centerEl]; @@ -282,7 +282,7 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { while (excerptLength < EXCERPT_CHAR_LIMIT) { let added = false; - const prev: Element | null = left.previousElementSibling; + const prev: HTMLElement | null = left.previousElementSibling; if (prev) { const prevText = prev.textContent || ""; @@ -290,7 +290,7 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { if (prevText.length + excerptLength > EXCERPT_CHAR_LIMIT) { const prefix = prevText.substr(prevText.length - (EXCERPT_CHAR_LIMIT - excerptLength)); - const textNode = document.createTextNode(`…${prefix}`); + const textNode = new TextNode(`…${prefix}`); excerptEls.unshift(textNode); break; @@ -302,7 +302,7 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { added = true; } - const next: Element | null = right.nextElementSibling; + const next: HTMLElement | null = right.nextElementSibling; if (next) { const nextText = next.textContent; @@ -310,7 +310,7 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { if (nextText && nextText.length + excerptLength > EXCERPT_CHAR_LIMIT) { const suffix = nextText.substr(nextText.length - (EXCERPT_CHAR_LIMIT - excerptLength)); - const textNode = document.createTextNode(`${suffix}…`); + const textNode = new TextNode(`${suffix}…`); excerptEls.push(textNode); break; @@ -327,7 +327,7 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { } } - const excerptWrapper = document.createElement("div"); + const excerptWrapper = new HTMLElement("div", {}); excerptWrapper.classList.add("ck-content"); excerptWrapper.classList.add("backlink-excerpt"); diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 237162200..cc5c5a0d9 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -1,4 +1,4 @@ -import { JSDOM } from "jsdom"; +import { parse, HTMLElement } from "node-html-parser"; import shaca from "./shaca/shaca.js"; import assetPath from "../services/asset_path.js"; import shareRoot from "./share_root.js"; @@ -65,8 +65,8 @@ function renderIndex(result: Result) { result.content += ""; } -export function renderText(result: Result, note: SNote) { - const document = new JSDOM(result.content || "").window.document; +function renderText(result: Result, note: SNote) { + const document = parse(result.content?.toString() || ""); // Process include notes. for (const includeNoteEl of document.querySelectorAll("section.include-note")) { @@ -79,11 +79,13 @@ export function renderText(result: Result, note: SNote) { const includedResult = getContent(note); if (typeof includedResult.content !== "string") continue; - const includedDocument = new JSDOM(includedResult.content).window.document; - includeNoteEl.replaceWith(...includedDocument.body.childNodes); + const includedDocument = parse(includedResult.content).querySelector("body"); + if (includedDocument) { + includeNoteEl.replaceWith(includedDocument); + } } - result.isEmpty = document.body.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0; + result.isEmpty = document.querySelector("body")?.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0; if (!result.isEmpty) { for (const linkEl of document.querySelectorAll("a")) { @@ -99,7 +101,7 @@ export function renderText(result: Result, note: SNote) { } } - result.content = document.body.innerHTML; + result.content = document.querySelector("body")?.innerHTML ?? ""; if (result.content.includes(``)) { result.header += ` @@ -120,7 +122,7 @@ document.addEventListener("DOMContentLoaded", function() { } } -function handleAttachmentLink(linkEl: HTMLAnchorElement, href: string) { +function handleAttachmentLink(linkEl: HTMLElement, href: string) { const linkRegExp = /attachmentId=([a-zA-Z0-9_]+)/g; let attachmentMatch; if ((attachmentMatch = linkRegExp.exec(href))) { @@ -131,7 +133,8 @@ function handleAttachmentLink(linkEl: HTMLAnchorElement, href: string) { linkEl.setAttribute("href", `api/attachments/${attachmentId}/download`); linkEl.classList.add(`attachment-link`); linkEl.classList.add(`role-${attachment.role}`); - linkEl.innerText = attachment.title; + linkEl.childNodes.length = 0; + linkEl.append(attachment.title); } else { linkEl.removeAttribute("href"); } @@ -164,11 +167,8 @@ export function renderCode(result: Result) { if (typeof result.content !== "string" || !result.content?.trim()) { result.isEmpty = true; } else { - const document = new JSDOM().window.document; - - const preEl = document.createElement("pre"); - preEl.appendChild(document.createTextNode(result.content)); - + const preEl = new HTMLElement("pre", {}); + preEl.append(result.content); result.content = preEl.outerHTML; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d93f7fcd8..6bdb2a38f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -449,6 +449,9 @@ importers: better-sqlite3: specifier: 12.4.1 version: 12.4.1 + node-html-parser: + specifier: 7.0.1 + version: 7.0.1 devDependencies: '@anthropic-ai/sdk': specifier: 0.64.0 @@ -513,9 +516,6 @@ importers: '@types/js-yaml': specifier: 4.0.9 version: 4.0.9 - '@types/jsdom': - specifier: 21.1.7 - version: 21.1.7 '@types/mime-types': specifier: 3.0.1 version: 3.0.1 @@ -681,9 +681,6 @@ importers: js-yaml: specifier: 4.1.0 version: 4.1.0 - jsdom: - specifier: 26.1.0 - version: 26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) marked: specifier: 16.3.0 version: 16.3.0 @@ -4809,9 +4806,6 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/jsdom@21.1.7': - resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -9971,6 +9965,9 @@ packages: node-html-parser@6.1.13: resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + node-html-parser@7.0.1: + resolution: {integrity: sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==} + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -14795,8 +14792,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.1.1 '@ckeditor/ckeditor5-watchdog': 46.1.1 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.2)': dependencies: @@ -14988,8 +14983,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.1.1 ckeditor5: 46.1.1(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-editor-multi-root@46.1.1': dependencies: @@ -15012,6 +15005,8 @@ snapshots: '@ckeditor/ckeditor5-table': 46.1.1 '@ckeditor/ckeditor5-utils': 46.1.1 ckeditor5: 46.1.1(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-emoji@46.1.1': dependencies: @@ -19131,12 +19126,6 @@ snapshots: '@types/js-yaml@4.0.9': {} - '@types/jsdom@21.1.7': - dependencies: - '@types/node': 22.15.21 - '@types/tough-cookie': 4.0.5 - parse5: 7.3.0 - '@types/json-schema@7.0.15': {} '@types/jsonfile@6.1.4': @@ -19346,7 +19335,8 @@ snapshots: '@types/tmp@0.2.6': {} - '@types/tough-cookie@4.0.5': {} + '@types/tough-cookie@4.0.5': + optional: true '@types/trusted-types@2.0.7': optional: true @@ -21272,7 +21262,7 @@ snapshots: css-select@4.3.0: dependencies: boolbase: 1.0.0 - css-what: 6.1.0 + css-what: 6.2.2 domhandler: 4.3.1 domutils: 2.8.0 nth-check: 2.1.1 @@ -25780,6 +25770,11 @@ snapshots: css-select: 5.2.2 he: 1.2.0 + node-html-parser@7.0.1: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + node-releases@2.0.19: {} node-releases@2.0.21: {} @@ -28888,7 +28883,7 @@ snapshots: dependencies: '@trysound/sax': 0.2.0 commander: 7.2.0 - css-select: 5.1.0 + css-select: 5.2.2 css-tree: 2.3.1 css-what: 6.1.0 csso: 5.0.5 diff --git a/scripts/build-utils.ts b/scripts/build-utils.ts index 8da5be9d1..6a08f112e 100644 --- a/scripts/build-utils.ts +++ b/scripts/build-utils.ts @@ -1,6 +1,6 @@ import { execSync } from "child_process"; import { build as esbuild } from "esbuild"; -import { cpSync, existsSync, rmSync } from "fs"; +import { cpSync, existsSync, rmSync, writeFileSync } from "fs"; import { copySync, emptyDirSync, mkdirpSync } from "fs-extra"; import { join } from "path"; @@ -37,7 +37,7 @@ export default class BuildHelper { } async buildBackend(entryPoints: string[]) { - await esbuild({ + const result = await esbuild({ entryPoints: entryPoints.map(e => join(this.projectDir, e)), tsconfig: join(this.projectDir, "tsconfig.app.json"), platform: "node", @@ -54,6 +54,7 @@ export default class BuildHelper { "./xhr-sync-worker.js", "vite" ], + metafile: true, splitting: false, loader: { ".css": "text", @@ -64,6 +65,7 @@ export default class BuildHelper { }, minify: true }); + writeFileSync(join(this.outDir, "meta.json"), JSON.stringify(result.metafile)); } triggerBuildAndCopyTo(projectToBuild: string, destPath: string) {