From 804fc72ed800641187dad7bd81066917a65e7b26 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 14:07:55 +0300 Subject: [PATCH 01/16] test(server/share): basic text rendering check --- .../server/src/share/content_renderer.spec.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/server/src/share/content_renderer.spec.ts b/apps/server/src/share/content_renderer.spec.ts index 85d1c9dde..80c754200 100644 --- a/apps/server/src/share/content_renderer.spec.ts +++ b/apps/server/src/share/content_renderer.spec.ts @@ -1,7 +1,31 @@ import { describe, it, expect } from "vitest"; -import { renderCode, type Result } from "./content_renderer.js"; +import { renderCode, renderText, type Result } from "./content_renderer.js"; +import { trimIndentation } from "@triliumnext/commons"; +import SNote from "./shaca/entities/snote.js"; describe("content_renderer", () => { + describe("renderText", () => { + it("parses simple note", () => { + const input = trimIndentation`\ +
+ +
+

+ + Welcome to Trilium Notes! + +

`; + + const result = { + content: input, + header: "", + isEmpty: false + }; + renderText(result, new SNote([ "root", "Note", "text", "text/plain", "1234", "2025-09-28T00:00Z", false])); + expect(result.content).toMatch(input); + }); + }); + describe("renderCode", () => { it("identifies empty content", () => { const emptyResult: Result = { From a393584a2a78bc1e0a9956279bdba2c56865e093 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 14:40:30 +0300 Subject: [PATCH 02/16] test(server/share): implement basic shaca mocking with content --- .../server/src/share/content_renderer.spec.ts | 20 ++-- apps/server/src/share/content_renderer.ts | 4 +- apps/server/src/test/shaca_mocking.ts | 97 +++++++++++++++++++ 3 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 apps/server/src/test/shaca_mocking.ts diff --git a/apps/server/src/share/content_renderer.spec.ts b/apps/server/src/share/content_renderer.spec.ts index 80c754200..04f60bd71 100644 --- a/apps/server/src/share/content_renderer.spec.ts +++ b/apps/server/src/share/content_renderer.spec.ts @@ -1,12 +1,12 @@ import { describe, it, expect } from "vitest"; -import { renderCode, renderText, type Result } from "./content_renderer.js"; +import { getContent, renderCode, renderText, type Result } from "./content_renderer.js"; import { trimIndentation } from "@triliumnext/commons"; -import SNote from "./shaca/entities/snote.js"; +import { buildShareNote } from "../test/shaca_mocking.js"; describe("content_renderer", () => { describe("renderText", () => { it("parses simple note", () => { - const input = trimIndentation`\ + const content = trimIndentation`\
@@ -15,14 +15,12 @@ describe("content_renderer", () => { Welcome to Trilium Notes!

`; - - const result = { - content: input, - header: "", - isEmpty: false - }; - renderText(result, new SNote([ "root", "Note", "text", "text/plain", "1234", "2025-09-28T00:00Z", false])); - expect(result.content).toMatch(input); + const note = buildShareNote({ + title: "Note", + content + }); + const result = getContent(note); + expect(result.content).toStrictEqual(content); }); }); diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index c12e7887f..253c43b39 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -16,7 +16,7 @@ export interface Result { isEmpty?: boolean; } -function getContent(note: SNote) { +export function getContent(note: SNote) { if (note.isProtected) { return { header: "", @@ -65,7 +65,7 @@ function renderIndex(result: Result) { result.content += ""; } -function renderText(result: Result, note: SNote) { +export function renderText(result: Result, note: SNote) { const document = new JSDOM(result.content || "").window.document; // Process include notes. diff --git a/apps/server/src/test/shaca_mocking.ts b/apps/server/src/test/shaca_mocking.ts new file mode 100644 index 000000000..e92411500 --- /dev/null +++ b/apps/server/src/test/shaca_mocking.ts @@ -0,0 +1,97 @@ +import utils from "../services/utils.js"; +import SAttribute from "../share/shaca/entities/sattribute.js"; +import SNote from "../share/shaca/entities/snote.js"; +import shaca from "../share/shaca/shaca.js"; + +type AttributeDefinitions = { [key in `#${string}`]: string; }; +type RelationDefinitions = { [key in `~${string}`]: string; }; + +interface NoteDefinition extends AttributeDefinitions, RelationDefinitions { + id?: string | undefined; + title: string; + content?: string | Buffer; +} + +/** + * Creates the given notes with the given title and optionally one or more attributes. + * + * For a label to be created, simply pass on a key prefixed with `#` and any desired value. + * + * The notes and attributes will be injected in the froca. + * + * @param notes + * @returns an array containing the IDs of the created notes. + * @example + * buildShareNotes([ + * { title: "A", "#startDate": "2025-05-05" }, + * { title: "B", "#startDate": "2025-05-07" } + * ]); + */ +export function buildShareNotes(notes: NoteDefinition[]) { + const ids: string[] = []; + + for (const noteDef of notes) { + ids.push(buildShareNote(noteDef).noteId); + } + + return ids; +} + +export function buildShareNote(noteDef: NoteDefinition) { + const blobId = "foo"; + const note = new SNote([ + noteDef.id ?? utils.randomString(12), + noteDef.title, + "text", + "text/html", + blobId, + new Date().toUTCString(), // utcDateModified + false // is protected + ]); + shaca.notes[note.noteId] = note; + + // Handle content + if (noteDef.content) { + note.getContent = () => noteDef.content; + } + + // Handle labels & relations + let position = 0; + for (const [ key, value ] of Object.entries(noteDef)) { + const attributeId = utils.randomString(12); + const name = key.substring(1); + + let attribute: SAttribute | null = null; + if (key.startsWith("#")) { + attribute = new SAttribute([ + attributeId, + note.noteId, + "label", + name, + value, + false, // isInheritable + position // position + ]); + } + + if (key.startsWith("~")) { + attribute = new SAttribute([ + attributeId, + note.noteId, + "relation", + name, + value, + false, // isInheritable + position // position + ]); + } + + if (!attribute) { + continue; + } + + shaca.attributes[attributeId] = attribute; + position++; + } + return note; +} From 1ad8b1bf853a89c15012d655cb2ed6cd7a7fae4f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 14:48:14 +0300 Subject: [PATCH 03/16] test(server/share): protected notes --- apps/server/src/share/content_renderer.spec.ts | 8 +++++++- apps/server/src/test/shaca_mocking.ts | 12 ++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/server/src/share/content_renderer.spec.ts b/apps/server/src/share/content_renderer.spec.ts index 04f60bd71..363b74390 100644 --- a/apps/server/src/share/content_renderer.spec.ts +++ b/apps/server/src/share/content_renderer.spec.ts @@ -4,7 +4,13 @@ import { trimIndentation } from "@triliumnext/commons"; import { buildShareNote } from "../test/shaca_mocking.js"; describe("content_renderer", () => { - describe("renderText", () => { + it("Reports protected notes not being renderable", () => { + const note = buildShareNote({ isProtected: true }); + const result = getContent(note); + expect(result.content).toStrictEqual("

Protected note cannot be displayed

"); + }); + + describe("Text note", () => { it("parses simple note", () => { const content = trimIndentation`\
diff --git a/apps/server/src/test/shaca_mocking.ts b/apps/server/src/test/shaca_mocking.ts index e92411500..022ee999a 100644 --- a/apps/server/src/test/shaca_mocking.ts +++ b/apps/server/src/test/shaca_mocking.ts @@ -8,8 +8,9 @@ type RelationDefinitions = { [key in `~${string}`]: string; }; interface NoteDefinition extends AttributeDefinitions, RelationDefinitions { id?: string | undefined; - title: string; + title?: string; content?: string | Buffer; + isProtected?: boolean; } /** @@ -41,18 +42,21 @@ export function buildShareNote(noteDef: NoteDefinition) { const blobId = "foo"; const note = new SNote([ noteDef.id ?? utils.randomString(12), - noteDef.title, + noteDef.title ?? "New note", "text", "text/html", blobId, new Date().toUTCString(), // utcDateModified - false // is protected + !!noteDef.isProtected ]); shaca.notes[note.noteId] = note; // Handle content if (noteDef.content) { - note.getContent = () => noteDef.content; + note.getContent = () => { + if (noteDef.isProtected) return undefined; + return noteDef.content; + }; } // Handle labels & relations From 8b5e53e5792a0cc77d7ebba3d442767d5a58fcec Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 15:07:10 +0300 Subject: [PATCH 04/16] test(server/share): included text notes --- .../server/src/share/content_renderer.spec.ts | 25 ++++++++++++++++++- apps/server/src/share/content_renderer.ts | 2 +- apps/server/src/test/shaca_mocking.ts | 10 ++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/server/src/share/content_renderer.spec.ts b/apps/server/src/share/content_renderer.spec.ts index 363b74390..137d85c97 100644 --- a/apps/server/src/share/content_renderer.spec.ts +++ b/apps/server/src/share/content_renderer.spec.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { getContent, renderCode, renderText, type Result } from "./content_renderer.js"; import { trimIndentation } from "@triliumnext/commons"; -import { buildShareNote } from "../test/shaca_mocking.js"; +import { buildShareNote, buildShareNotes } from "../test/shaca_mocking.js"; describe("content_renderer", () => { it("Reports protected notes not being renderable", () => { @@ -28,6 +28,29 @@ describe("content_renderer", () => { const result = getContent(note); expect(result.content).toStrictEqual(content); }); + + it("renders included notes", () => { + buildShareNotes([ + { id: "subnote1", content: `

Foo

Bar
` }, + { id: "subnote2", content: `Baz` } + ]); + const note = buildShareNote({ + id: "note1", + content: trimIndentation`\ +

Before

+
 
+
 
+

After

+ ` + }); + const result = getContent(note); + expect(result.content).toStrictEqual(trimIndentation`\ +

Before

+

Foo

Bar
+ Baz +

After

+ `); + }); }); describe("renderCode", () => { diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 253c43b39..237162200 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -80,7 +80,7 @@ export function renderText(result: Result, note: SNote) { if (typeof includedResult.content !== "string") continue; const includedDocument = new JSDOM(includedResult.content).window.document; - includeNoteEl.replaceWith(includedDocument.body); + includeNoteEl.replaceWith(...includedDocument.body.childNodes); } result.isEmpty = document.body.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0; diff --git a/apps/server/src/test/shaca_mocking.ts b/apps/server/src/test/shaca_mocking.ts index 022ee999a..af02394a0 100644 --- a/apps/server/src/test/shaca_mocking.ts +++ b/apps/server/src/test/shaca_mocking.ts @@ -10,6 +10,7 @@ interface NoteDefinition extends AttributeDefinitions, RelationDefinitions { id?: string | undefined; title?: string; content?: string | Buffer; + children?: NoteDefinition[]; isProtected?: boolean; } @@ -59,6 +60,15 @@ export function buildShareNote(noteDef: NoteDefinition) { }; } + // Handle children. + if (noteDef.children) { + for (const childDef of noteDef.children) { + const childNote = buildShareNote(childDef); + + // TODO: Create corresponding SBranch. + } + } + // Handle labels & relations let position = 0; for (const [ key, value ] of Object.entries(noteDef)) { From 614a8f177c9f806d8558209816b3f3b2d108d191 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 15:43:39 +0300 Subject: [PATCH 05/16] test(server/share): attachment links --- .../server/src/share/content_renderer.spec.ts | 31 ++++++++++++++++--- apps/server/src/test/shaca_mocking.ts | 24 ++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/apps/server/src/share/content_renderer.spec.ts b/apps/server/src/share/content_renderer.spec.ts index 137d85c97..c8601fe57 100644 --- a/apps/server/src/share/content_renderer.spec.ts +++ b/apps/server/src/share/content_renderer.spec.ts @@ -21,14 +21,37 @@ describe("content_renderer", () => { Welcome to Trilium Notes!

`; - const note = buildShareNote({ - title: "Note", - content - }); + const note = buildShareNote({ content }); const result = getContent(note); expect(result.content).toStrictEqual(content); }); + it("handles attachment link", () => { + const content = trimIndentation`\ +

Test

+

+ + 5863845791835102555.mp4 + +   +

+ `; + const note = buildShareNote({ + content, + attachments: [ { id: "q14s2Id7V6pp" } ] + }); + const result = getContent(note); + expect(result.content).toStrictEqual(trimIndentation`\ +

Test

+

+ + 5863845791835102555.mp4 + +   +

+ `); + }); + it("renders included notes", () => { buildShareNotes([ { id: "subnote1", content: `

Foo

Bar
` }, diff --git a/apps/server/src/test/shaca_mocking.ts b/apps/server/src/test/shaca_mocking.ts index af02394a0..8b2068a0e 100644 --- a/apps/server/src/test/shaca_mocking.ts +++ b/apps/server/src/test/shaca_mocking.ts @@ -1,4 +1,5 @@ import utils from "../services/utils.js"; +import SAttachment from "../share/shaca/entities/sattachment.js"; import SAttribute from "../share/shaca/entities/sattribute.js"; import SNote from "../share/shaca/entities/snote.js"; import shaca from "../share/shaca/shaca.js"; @@ -6,11 +7,19 @@ import shaca from "../share/shaca/shaca.js"; type AttributeDefinitions = { [key in `#${string}`]: string; }; type RelationDefinitions = { [key in `~${string}`]: string; }; +interface AttachementDefinition { + id?: string; + role?: string; + mime?: string; + title?: string; +} + interface NoteDefinition extends AttributeDefinitions, RelationDefinitions { id?: string | undefined; title?: string; content?: string | Buffer; children?: NoteDefinition[]; + attachments?: AttachementDefinition[]; isProtected?: boolean; } @@ -60,6 +69,21 @@ export function buildShareNote(noteDef: NoteDefinition) { }; } + // Handle attachments. + if (noteDef.attachments) { + for (const attachmentDef of noteDef.attachments) { + new SAttachment([ + attachmentDef.id ?? utils.randomString(12), + note.noteId, + attachmentDef.role ?? "file", + attachmentDef.mime ?? "application/blob", + attachmentDef.title ?? "New attachment", + blobId, + new Date().toUTCString(), // utcDateModified + ]); + } + } + // Handle children. if (noteDef.children) { for (const childDef of noteDef.children) { From 151a2c284d2e8b32867c3e4fe46be7d39fedae19 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 17:12:01 +0300 Subject: [PATCH 06/16] test(server/note_map): backlinks with excerpts --- apps/server/src/routes/api/note_map.spec.ts | 99 +++++++++++++++++++++ apps/server/src/routes/api/note_map.ts | 2 +- apps/server/src/test/becca_easy_mocking.ts | 92 +++++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/routes/api/note_map.spec.ts create mode 100644 apps/server/src/test/becca_easy_mocking.ts diff --git a/apps/server/src/routes/api/note_map.spec.ts b/apps/server/src/routes/api/note_map.spec.ts new file mode 100644 index 000000000..c3696fc3a --- /dev/null +++ b/apps/server/src/routes/api/note_map.spec.ts @@ -0,0 +1,99 @@ +import { trimIndentation } from "@triliumnext/commons"; +import { buildNote, buildNotes } from "../../test/becca_easy_mocking"; +import note_map from "./note_map"; + +describe("Note map service", () => { + it("correctly identifies backlinks", () => { + const note = buildNote({ id: "dUtgloZIckax", title: "Backlink text" }); + buildNotes([ + { + title: "First", + id: "first", + "~internalLink": "dUtgloZIckax", + content: trimIndentation`\ +

+ The quick brownie +

+

+ + Backlink text + +

+
+ +
+ ` + }, + { + title: "Second", + id: "second", + "~internalLink": "dUtgloZIckax", + content: trimIndentation`\ +

+ + Backlink text + +

+

+ + First + +

+

+ + Second + +

+ ` + } + ]); + + const backlinksResponse = note_map.getBacklinks({ + params: { + noteId: note.noteId + } + } as any); + expect(backlinksResponse).toMatchObject([ + { + excerpts: [ + trimIndentation`\ + ` + ], + noteId: "first", + }, + { + excerpts: [ + trimIndentation`\ + ` + ], + noteId: "second" + } + ]); + }); +}); diff --git a/apps/server/src/routes/api/note_map.ts b/apps/server/src/routes/api/note_map.ts index 3a2bed5b2..616724960 100644 --- a/apps/server/src/routes/api/note_map.ts +++ b/apps/server/src/routes/api/note_map.ts @@ -251,7 +251,7 @@ function removeImages(document: Document) { const EXCERPT_CHAR_LIMIT = 200; type ElementOrText = Element | Text; -function findExcerpts(sourceNote: BNote, referencedNoteId: string) { +export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { const html = sourceNote.getContent(); const document = new JSDOM(html).window.document; diff --git a/apps/server/src/test/becca_easy_mocking.ts b/apps/server/src/test/becca_easy_mocking.ts new file mode 100644 index 000000000..579ded515 --- /dev/null +++ b/apps/server/src/test/becca_easy_mocking.ts @@ -0,0 +1,92 @@ +import utils from "../services/utils.js"; +import BNote from "../becca/entities/bnote.js"; +import BAttribute from "../becca/entities/battribute.js"; + +type AttributeDefinitions = { [key in `#${string}`]: string; }; +type RelationDefinitions = { [key in `~${string}`]: string; }; + +interface NoteDefinition extends AttributeDefinitions, RelationDefinitions { + id?: string | undefined; + title: string; + content?: string; +} + +/** + * Creates the given notes with the given title and optionally one or more attributes. + * + * For a label to be created, simply pass on a key prefixed with `#` and any desired value. + * + * The notes and attributes will be injected in the froca. + * + * @param notes + * @returns an array containing the IDs of the created notes. + * @example + * buildNotes([ + * { title: "A", "#startDate": "2025-05-05" }, + * { title: "B", "#startDate": "2025-05-07" } + * ]); + */ +export function buildNotes(notes: NoteDefinition[]) { + const ids: string[] = []; + + for (const noteDef of notes) { + ids.push(buildNote(noteDef).noteId); + } + + return ids; +} + +export function buildNote(noteDef: NoteDefinition) { + const note = new BNote({ + noteId: noteDef.id ?? utils.randomString(12), + title: noteDef.title, + type: "text", + mime: "text/html", + isProtected: false, + blobId: "" + }); + + // Handle content. + if (noteDef.content) { + note.getContent = () => noteDef.content!; + } + + // Handle labels and relations. + let position = 0; + for (const [ key, value ] of Object.entries(noteDef)) { + const attributeId = utils.randomString(12); + const name = key.substring(1); + + let attribute: BAttribute | null = null; + if (key.startsWith("#")) { + attribute = new BAttribute({ + noteId: note.noteId, + attributeId, + type: "label", + name, + value, + position, + isInheritable: false + }); + } + + if (key.startsWith("~")) { + attribute = new BAttribute({ + noteId: note.noteId, + attributeId, + type: "relation", + name, + value, + position, + isInheritable: false + }); + } + + if (!attribute) { + continue; + } + + position++; + } + return note; +} From 517bfd2c9a4fb61cd3689e72e202ae87f51ac552 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 18:54:57 +0300 Subject: [PATCH 07/16] test(server/note_map): clipper processing notes & images --- apps/server/src/routes/api/clipper.spec.ts | 50 ++++++++++++++++++++++ apps/server/src/routes/api/clipper.ts | 2 +- apps/server/src/test/becca_easy_mocking.ts | 4 +- 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 apps/server/src/routes/api/clipper.spec.ts diff --git a/apps/server/src/routes/api/clipper.spec.ts b/apps/server/src/routes/api/clipper.spec.ts new file mode 100644 index 000000000..6df445dd0 --- /dev/null +++ b/apps/server/src/routes/api/clipper.spec.ts @@ -0,0 +1,50 @@ +import { BNote } from "../../services/backend_script_entrypoint"; +import { buildNote } from "../../test/becca_easy_mocking"; +import { processContent } from "./clipper"; + +let note!: BNote; + +describe("processContent", () => { + beforeAll(() => { + note = buildNote({}); + note.saveAttachment = () => {}; + vi.mock("../../services/image.js", () => ({ + default: { + saveImageToAttachment() { + return { + attachmentId: "foo", + title: "encodedTitle", + } + } + } + })); + }); + + it("processes basic note", () => { + const processed = processContent([], note, "

Hello world.

"); + expect(processed).toStrictEqual("

Hello world.

") + }); + + it("processes plain text", () => { + const processed = processContent([], note, "Hello world."); + expect(processed).toStrictEqual("

Hello world.

") + }); + + it("replaces images", () => { + const processed = processContent( + [{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAF0lEQVQoU2P8DwQMBADjqKLRIGAgKggAzHs/0SoYCGwAAAAASUVORK5CYII="}], + note, `` + ); + expect(processed).toStrictEqual(``); + }); + + it("skips over non-data images", () => { + for (const url of [ "foo", "" ]) { + const processed = processContent( + [{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl": url}], + note, `` + ); + expect(processed).toStrictEqual(``); + } + }); +}); diff --git a/apps/server/src/routes/api/clipper.ts b/apps/server/src/routes/api/clipper.ts index 2535a26e2..0b87aabd6 100644 --- a/apps/server/src/routes/api/clipper.ts +++ b/apps/server/src/routes/api/clipper.ts @@ -147,7 +147,7 @@ async function createNote(req: Request) { }; } -function processContent(images: Image[], note: BNote, content: string) { +export function processContent(images: Image[], note: BNote, content: string) { let rewrittenContent = htmlSanitizer.sanitize(content); if (images) { diff --git a/apps/server/src/test/becca_easy_mocking.ts b/apps/server/src/test/becca_easy_mocking.ts index 579ded515..6df198a5a 100644 --- a/apps/server/src/test/becca_easy_mocking.ts +++ b/apps/server/src/test/becca_easy_mocking.ts @@ -7,7 +7,7 @@ type RelationDefinitions = { [key in `~${string}`]: string; }; interface NoteDefinition extends AttributeDefinitions, RelationDefinitions { id?: string | undefined; - title: string; + title?: string; content?: string; } @@ -39,7 +39,7 @@ export function buildNotes(notes: NoteDefinition[]) { export function buildNote(noteDef: NoteDefinition) { const note = new BNote({ noteId: noteDef.id ?? utils.randomString(12), - title: noteDef.title, + title: noteDef.title ?? "New note", type: "text", mime: "text/html", isProtected: false, From 290d134d881648fafccb5eef4cce13dcfb4d4cf1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 19:10:49 +0300 Subject: [PATCH 08/16] test(server/similarity): reward map --- apps/server/src/becca/similarity.spec.ts | 20 ++++++++++++++++++++ apps/server/src/becca/similarity.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/becca/similarity.spec.ts diff --git a/apps/server/src/becca/similarity.spec.ts b/apps/server/src/becca/similarity.spec.ts new file mode 100644 index 000000000..4809e1f13 --- /dev/null +++ b/apps/server/src/becca/similarity.spec.ts @@ -0,0 +1,20 @@ +import { trimIndentation } from "@triliumnext/commons"; +import { buildNote } from "../test/becca_easy_mocking"; +import { buildRewardMap } from "./similarity"; + +describe("buildRewardMap", () => { + it("calculates heading rewards", () => { + const note = buildNote({ + content: trimIndentation`\ +

Heading 1

+

Heading 2

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eget purus et eros faucibus dignissim. Vestibulum lacinia urna quis eleifend consectetur. Aenean elementum pellentesque ultrices. Donec tincidunt, felis vel pretium suscipit, nibh lorem gravida est, quis tincidunt metus nibh a tortor. Aenean erat libero, faucibus ac mattis non, imperdiet eget nunc. Pellentesque aliquam molestie nibh eu interdum. Sed augue velit, varius id lacinia ut, dictum in dolor. Praesent posuere quam vel porta eleifend. Nullam porta tempus convallis. Aliquam auctor dui nec consectetur suscipit. Mauris laoreet commodo dapibus. Donec sodales justo velit, at placerat nulla cursus sit amet. Aliquam erat volutpat. Donec nec mauris iaculis, ullamcorper lectus et, feugiat arcu. Nunc vel ligula quis lectus efficitur porta non at nulla.

+

Heading 3

+ ` + }); + const map = buildRewardMap(note); + for (const key of [ "new", "note", "heading", "1", "2", "3" ]) { + expect(typeof map.get(key)).toStrictEqual("number"); + } + }); +}); diff --git a/apps/server/src/becca/similarity.ts b/apps/server/src/becca/similarity.ts index dc0a4c3f8..11bfd93ed 100644 --- a/apps/server/src/becca/similarity.ts +++ b/apps/server/src/becca/similarity.ts @@ -44,7 +44,7 @@ function filterUrlValue(value: string) { .replace(/(\.net|\.com|\.org|\.info|\.edu)/gi, ""); } -function buildRewardMap(note: BNote) { +export function buildRewardMap(note: BNote) { // Need to use Map instead of object: https://github.com/zadam/trilium/issues/1895 const map = new Map(); From c0ea441c59479b73ff912e1b6d669ee94d0af745 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 19:19:10 +0300 Subject: [PATCH 09/16] chore(server): fix tests --- apps/server/src/share/content_renderer.spec.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/server/src/share/content_renderer.spec.ts b/apps/server/src/share/content_renderer.spec.ts index c8601fe57..aaa4316f2 100644 --- a/apps/server/src/share/content_renderer.spec.ts +++ b/apps/server/src/share/content_renderer.spec.ts @@ -1,9 +1,18 @@ import { describe, it, expect } from "vitest"; -import { getContent, renderCode, renderText, type Result } from "./content_renderer.js"; +import { getContent, renderCode, type Result } from "./content_renderer.js"; import { trimIndentation } from "@triliumnext/commons"; import { buildShareNote, buildShareNotes } from "../test/shaca_mocking.js"; describe("content_renderer", () => { + beforeAll(() => { + vi.mock("../becca/becca_loader.js", () => ({ + default: { + load: vi.fn(), + loaded: Promise.resolve() + } + })); + }); + it("Reports protected notes not being renderable", () => { const note = buildShareNote({ isProtected: true }); const result = getContent(note); From 7e79d907be8d73e9b6df3a9a972e1cb1234e8336 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 19:43:04 +0300 Subject: [PATCH 10/16] feat(server): replace jsdom --- apps/desktop/scripts/build.ts | 1 - apps/server/package.json | 5 ++- apps/server/scripts/build.ts | 1 - apps/server/src/becca/similarity.ts | 6 ++-- apps/server/src/routes/api/clipper.ts | 7 ++-- apps/server/src/routes/api/note_map.ts | 22 ++++++------- apps/server/src/share/content_renderer.ts | 28 ++++++++-------- pnpm-lock.yaml | 39 ++++++++++------------- scripts/build-utils.ts | 6 ++-- 9 files changed, 54 insertions(+), 61 deletions(-) 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) { From 2a0410f597fc1da51a12227db508d9037faa2454 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 28 Sep 2025 20:55:20 +0300 Subject: [PATCH 11/16] chore(types): fill in gap from jsdom --- apps/client/tsconfig.app.json | 2 +- tsconfig.base.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/tsconfig.app.json b/apps/client/tsconfig.app.json index 16b93e1ce..4abc845e4 100644 --- a/apps/client/tsconfig.app.json +++ b/apps/client/tsconfig.app.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "lib": [ "ESNext" ], + "lib": [ "ESNext", "DOM.Iterable" ], "outDir": "dist", "types": [ "node", diff --git a/tsconfig.base.json b/tsconfig.base.json index 76d2e73fc..22906a79f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -5,7 +5,7 @@ "emitDeclarationOnly": true, "importHelpers": true, "isolatedModules": true, - "lib": ["ES2023"], + "lib": ["ES2023", "DOM", "DOM.Iterable"], "module": "nodenext", "moduleResolution": "nodenext", "noEmitOnError": true, From 4c6a742af76153d1f8dfb609682deaa757418d51 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 29 Sep 2025 09:25:31 +0300 Subject: [PATCH 12/16] chore(server): fix clipper --- apps/server/src/routes/api/clipper.spec.ts | 4 ++-- apps/server/src/routes/api/clipper.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/routes/api/clipper.spec.ts b/apps/server/src/routes/api/clipper.spec.ts index 6df445dd0..1efa6915e 100644 --- a/apps/server/src/routes/api/clipper.spec.ts +++ b/apps/server/src/routes/api/clipper.spec.ts @@ -35,7 +35,7 @@ describe("processContent", () => { [{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAF0lEQVQoU2P8DwQMBADjqKLRIGAgKggAzHs/0SoYCGwAAAAASUVORK5CYII="}], note, `` ); - expect(processed).toStrictEqual(``); + expect(processed).toStrictEqual(``); }); it("skips over non-data images", () => { @@ -44,7 +44,7 @@ describe("processContent", () => { [{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl": url}], note, `` ); - expect(processed).toStrictEqual(``); + expect(processed).toStrictEqual(``); } }); }); diff --git a/apps/server/src/routes/api/clipper.ts b/apps/server/src/routes/api/clipper.ts index 7f6a39f9d..133c35a88 100644 --- a/apps/server/src/routes/api/clipper.ts +++ b/apps/server/src/routes/api/clipper.ts @@ -183,7 +183,7 @@ export function processContent(images: Image[], note: BNote, content: string) { const dom = parse(rewrittenContent); // Get the content inside the body tag and serialize it - rewrittenContent = dom.querySelector("body")?.innerHTML ?? ""; + rewrittenContent = dom.innerHTML ?? ""; return rewrittenContent; } From f718e876739757d734ced91b4a15bad7c4ee94cb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 29 Sep 2025 09:25:46 +0300 Subject: [PATCH 13/16] chore(server): fix share content renderer --- apps/server/src/share/content_renderer.spec.ts | 6 ++---- apps/server/src/share/content_renderer.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/server/src/share/content_renderer.spec.ts b/apps/server/src/share/content_renderer.spec.ts index aaa4316f2..8f3f70622 100644 --- a/apps/server/src/share/content_renderer.spec.ts +++ b/apps/server/src/share/content_renderer.spec.ts @@ -47,15 +47,13 @@ describe("content_renderer", () => { `; const note = buildShareNote({ content, - attachments: [ { id: "q14s2Id7V6pp" } ] + attachments: [ { id: "q14s2Id7V6pp", title: "5863845791835102555.mp4" } ] }); const result = getContent(note); expect(result.content).toStrictEqual(trimIndentation`\

Test

- - 5863845791835102555.mp4 - + 5863845791835102555.mp4  

`); diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index cc5c5a0d9..35fb841b0 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -1,4 +1,4 @@ -import { parse, HTMLElement } from "node-html-parser"; +import { parse, HTMLElement, TextNode } from "node-html-parser"; import shaca from "./shaca/shaca.js"; import assetPath from "../services/asset_path.js"; import shareRoot from "./share_root.js"; @@ -79,13 +79,13 @@ function renderText(result: Result, note: SNote) { const includedResult = getContent(note); if (typeof includedResult.content !== "string") continue; - const includedDocument = parse(includedResult.content).querySelector("body"); + const includedDocument = parse(includedResult.content).childNodes; if (includedDocument) { - includeNoteEl.replaceWith(includedDocument); + includeNoteEl.replaceWith(...includedDocument); } } - result.isEmpty = document.querySelector("body")?.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0; + result.isEmpty = document.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0; if (!result.isEmpty) { for (const linkEl of document.querySelectorAll("a")) { @@ -101,7 +101,7 @@ function renderText(result: Result, note: SNote) { } } - result.content = document.querySelector("body")?.innerHTML ?? ""; + result.content = document.innerHTML ?? ""; if (result.content.includes(``)) { result.header += ` @@ -134,7 +134,7 @@ function handleAttachmentLink(linkEl: HTMLElement, href: string) { linkEl.classList.add(`attachment-link`); linkEl.classList.add(`role-${attachment.role}`); linkEl.childNodes.length = 0; - linkEl.append(attachment.title); + linkEl.appendChild(new TextNode(attachment.title)); } else { linkEl.removeAttribute("href"); } @@ -168,7 +168,7 @@ export function renderCode(result: Result) { result.isEmpty = true; } else { const preEl = new HTMLElement("pre", {}); - preEl.append(result.content); + preEl.appendChild(new TextNode(result.content)); result.content = preEl.outerHTML; } } From 58a883797d358179c2e8161740d2d14380ce41e0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 29 Sep 2025 09:45:16 +0300 Subject: [PATCH 14/16] fix(server): infinite loop in note map --- apps/server/src/routes/api/note_map.spec.ts | 8 ++++---- apps/server/src/routes/api/note_map.ts | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/server/src/routes/api/note_map.spec.ts b/apps/server/src/routes/api/note_map.spec.ts index c3696fc3a..0ab5d1d2d 100644 --- a/apps/server/src/routes/api/note_map.spec.ts +++ b/apps/server/src/routes/api/note_map.spec.ts @@ -57,7 +57,7 @@ describe("Note map service", () => { { excerpts: [ trimIndentation`\ - ` ], noteId: "first", }, { excerpts: [ trimIndentation`\ - ` ], noteId: "second" } diff --git a/apps/server/src/routes/api/note_map.ts b/apps/server/src/routes/api/note_map.ts index 6547ad6b9..84161cacc 100644 --- a/apps/server/src/routes/api/note_map.ts +++ b/apps/server/src/routes/api/note_map.ts @@ -243,8 +243,8 @@ function updateDescendantCountMapForSearch(noteIdToDescendantCountMap: Record 0) { - images[0]?.parentNode?.removeChild(images[0]); + for (const image of images) { + image.remove(); } } @@ -257,9 +257,13 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { const excerpts: string[] = []; + console.log("Removing images") removeImages(document); + console.log("Querying links"); + for (const linkEl of document.querySelectorAll("a")) { + console.log("Got ", linkEl.innerHTML); const href = linkEl.getAttribute("href"); if (!href || !href.endsWith(referencedNoteId)) { @@ -271,6 +275,7 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { let centerEl: HTMLElement = linkEl; while (centerEl.tagName !== "BODY" && centerEl.parentNode && (centerEl.parentNode?.textContent?.length || 0) <= EXCERPT_CHAR_LIMIT) { + console.log("Got ", centerEl.tagName, centerEl.parentNode); centerEl = centerEl.parentNode; } @@ -366,6 +371,7 @@ function getBacklinks(req: Request): BacklinksResponse { let backlinksWithExcerptCount = 0; return getFilteredBacklinks(note).map((backlink) => { + console.log("Processing ", backlink); const sourceNote = backlink.note; if (sourceNote.type !== "text" || backlinksWithExcerptCount > 50) { From 979ef6287f126d66e5be7449d47b023215188765 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 29 Sep 2025 09:55:34 +0300 Subject: [PATCH 15/16] chore(server): different handling of buffer vs string --- apps/server/src/becca/similarity.ts | 4 +++- apps/server/src/routes/api/note_map.ts | 5 ----- apps/server/src/share/content_renderer.ts | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/server/src/becca/similarity.ts b/apps/server/src/becca/similarity.ts index a024fc455..10a0e706d 100644 --- a/apps/server/src/becca/similarity.ts +++ b/apps/server/src/becca/similarity.ts @@ -123,7 +123,9 @@ export function buildRewardMap(note: BNote) { if (note.type === "text" && note.isDecrypted) { const content = note.getContent(); - const dom = parse(content.toString()); + if (typeof content !== "string") return map; + + const dom = parse(content); const addHeadingsToRewardMap = (elName: string, rewardFactor: number) => { for (const el of dom.querySelectorAll(elName)) { diff --git a/apps/server/src/routes/api/note_map.ts b/apps/server/src/routes/api/note_map.ts index 84161cacc..473388af0 100644 --- a/apps/server/src/routes/api/note_map.ts +++ b/apps/server/src/routes/api/note_map.ts @@ -257,11 +257,8 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { const excerpts: string[] = []; - console.log("Removing images") removeImages(document); - console.log("Querying links"); - for (const linkEl of document.querySelectorAll("a")) { console.log("Got ", linkEl.innerHTML); const href = linkEl.getAttribute("href"); @@ -275,7 +272,6 @@ export function findExcerpts(sourceNote: BNote, referencedNoteId: string) { let centerEl: HTMLElement = linkEl; while (centerEl.tagName !== "BODY" && centerEl.parentNode && (centerEl.parentNode?.textContent?.length || 0) <= EXCERPT_CHAR_LIMIT) { - console.log("Got ", centerEl.tagName, centerEl.parentNode); centerEl = centerEl.parentNode; } @@ -371,7 +367,6 @@ function getBacklinks(req: Request): BacklinksResponse { let backlinksWithExcerptCount = 0; return getFilteredBacklinks(note).map((backlink) => { - console.log("Processing ", backlink); const sourceNote = backlink.note; if (sourceNote.type !== "text" || backlinksWithExcerptCount > 50) { diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 35fb841b0..83ddfde2e 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -66,7 +66,8 @@ function renderIndex(result: Result) { } function renderText(result: Result, note: SNote) { - const document = parse(result.content?.toString() || ""); + if (typeof result.content !== "string") return; + const document = parse(result.content || ""); // Process include notes. for (const includeNoteEl of document.querySelectorAll("section.include-note")) { From acb98061ce82ca371968aad132d05449c972eb9f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 29 Sep 2025 09:59:39 +0300 Subject: [PATCH 16/16] chore(deps): remove jsdom as dev dependency --- package.json | 1 - .../express-partial-content/vite.config.ts | 2 +- pnpm-lock.yaml | 41 ++++++++++++++----- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 5ed05401d..db1abf06c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "eslint-plugin-react-hooks": "5.2.0", "happy-dom": "~19.0.0", "jiti": "2.6.0", - "jsdom": "~26.1.0", "jsonc-eslint-parser": "^2.1.0", "react-refresh": "^0.17.0", "rollup-plugin-webpack-stats": "2.1.5", diff --git a/packages/express-partial-content/vite.config.ts b/packages/express-partial-content/vite.config.ts index e813c9ac1..fc85d4a52 100644 --- a/packages/express-partial-content/vite.config.ts +++ b/packages/express-partial-content/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig(() => ({ test: { watch: false, globals: true, - environment: 'jsdom', + environment: 'happy-dom', include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2790c6a22..b73a74d4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,9 +88,6 @@ importers: jiti: specifier: 2.6.0 version: 2.6.0 - jsdom: - specifier: ~26.1.0 - version: 26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) jsonc-eslint-parser: specifier: ^2.1.0 version: 2.4.1 @@ -13939,6 +13936,7 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 + optional: true '@aws-crypto/crc32@5.2.0': dependencies: @@ -14725,6 +14723,8 @@ snapshots: '@ckeditor/ckeditor5-core': 46.1.1 '@ckeditor/ckeditor5-utils': 46.1.1 ckeditor5: 46.1.1(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@46.1.1(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -14784,6 +14784,8 @@ 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: @@ -14975,6 +14977,8 @@ 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: @@ -15470,6 +15474,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.1.1 '@ckeditor/ckeditor5-utils': 46.1.1 ckeditor5: 46.1.1(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-restricted-editing@46.1.1': dependencies: @@ -15556,8 +15562,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.1.1 '@ckeditor/ckeditor5-utils': 46.1.1 ckeditor5: 46.1.1(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-special-characters@46.1.1': dependencies: @@ -15871,12 +15875,14 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@csstools/color-helpers@5.0.2': {} + '@csstools/color-helpers@5.0.2': + optional: true '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + optional: true '@csstools/css-color-parser@3.0.9(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: @@ -15884,6 +15890,7 @@ snapshots: '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + optional: true '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: @@ -21506,6 +21513,7 @@ snapshots: dependencies: '@asamuzakjp/css-color': 3.1.4 rrweb-cssom: 0.8.0 + optional: true csstype@3.1.3: optional: true @@ -21720,6 +21728,7 @@ snapshots: dependencies: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + optional: true data-view-buffer@1.0.2: dependencies: @@ -23676,6 +23685,7 @@ snapshots: html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 + optional: true html-escaper@2.0.2: {} @@ -24413,6 +24423,7 @@ snapshots: - bufferutil - supports-color - utf-8-validate + optional: true jsesc@3.1.0: {} @@ -27803,7 +27814,8 @@ snapshots: transitivePeerDependencies: - supports-color - rrweb-cssom@0.8.0: {} + rrweb-cssom@0.8.0: + optional: true run-applescript@7.1.0: {} @@ -27979,6 +27991,7 @@ snapshots: saxes@6.0.0: dependencies: xmlchars: 2.2.0 + optional: true scheduler@0.19.1: dependencies: @@ -29119,11 +29132,13 @@ snapshots: tinyspy@4.0.3: {} - tldts-core@6.1.86: {} + tldts-core@6.1.86: + optional: true tldts@6.1.86: dependencies: tldts-core: 6.1.86 + optional: true tmp-promise@3.0.3: dependencies: @@ -29172,6 +29187,7 @@ snapshots: tough-cookie@5.1.2: dependencies: tldts: 6.1.86 + optional: true tr46@0.0.3: {} @@ -29182,6 +29198,7 @@ snapshots: tr46@5.1.1: dependencies: punycode: 2.3.1 + optional: true tree-dump@1.1.0(tslib@2.8.1): dependencies: @@ -29804,6 +29821,7 @@ snapshots: w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 + optional: true wait-port@1.1.0: dependencies: @@ -29892,7 +29910,8 @@ snapshots: webidl-conversions@6.1.0: {} - webidl-conversions@7.0.0: {} + webidl-conversions@7.0.0: + optional: true webpack-dev-middleware@7.4.3(webpack@5.101.3(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.25.10)): dependencies: @@ -30053,6 +30072,7 @@ snapshots: dependencies: tr46: 5.1.1 webidl-conversions: 7.0.0 + optional: true whatwg-url@5.0.0: dependencies: @@ -30208,7 +30228,8 @@ snapshots: xml-name-validator@3.0.0: {} - xml-name-validator@5.0.0: {} + xml-name-validator@5.0.0: + optional: true xml-parse-from-string@1.0.1: {}