Replace jsdom with node-html-parser (#7128)

This commit is contained in:
Elian Doran 2025-09-29 10:47:33 +03:00 committed by GitHub
commit 5e1cd7d6ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 573 additions and 77 deletions

View File

@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": [ "ESNext" ],
"lib": [ "ESNext", "DOM.Iterable" ],
"outDir": "dist",
"types": [
"node",

View File

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

View File

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

View File

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

View File

@ -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`\
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<p>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.</p>
<h3>Heading 3</h3>
`
});
const map = buildRewardMap(note);
for (const key of [ "new", "note", "heading", "1", "2", "3" ]) {
expect(typeof map.get(key)).toStrictEqual("number");
}
});
});

View File

@ -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";
@ -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();
@ -123,10 +123,12 @@ function buildRewardMap(note: BNote) {
if (note.type === "text" && note.isDecrypted) {
const content = note.getContent();
const dom = new JSDOM(content);
if (typeof content !== "string") return map;
const dom = parse(content);
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);
}
};

View File

@ -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, "<p>Hello world.</p>");
expect(processed).toStrictEqual("<p>Hello world.</p>")
});
it("processes plain text", () => {
const processed = processContent([], note, "Hello world.");
expect(processed).toStrictEqual("<p>Hello world.</p>")
});
it("replaces images", () => {
const processed = processContent(
[{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAF0lEQVQoU2P8DwQMBADjqKLRIGAgKggAzHs/0SoYCGwAAAAASUVORK5CYII="}],
note, `<img src="OKZxZA3MonZJkwFcEhId">`
);
expect(processed).toStrictEqual(`<img src="api/attachments/foo/image/encodedTitle" >`);
});
it("skips over non-data images", () => {
for (const url of [ "foo", "" ]) {
const processed = processContent(
[{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl": url}],
note, `<img src="OKZxZA3MonZJkwFcEhId">`
);
expect(processed).toStrictEqual(`<img src="OKZxZA3MonZJkwFcEhId" >`);
}
});
});

View File

@ -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;
@ -147,7 +146,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) {
@ -181,10 +180,10 @@ function processContent(images: Image[], note: BNote, content: string) {
rewrittenContent = `<p>${rewrittenContent}</p>`;
}
// 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.innerHTML ?? "";
return rewrittenContent;
}

View File

@ -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`\
<p>
The quick brownie
</p>
<p>
<a class="reference-link" href="#root/dUtgloZIckax">
Backlink text
</a>
</p>
<figure class="image">
<img style="aspect-ratio:960/1280;" src="api/attachments/llY9IHS3ZSqE/image/5877566469045340078_121.jpg" width="960" height="1280">
</figure>
`
},
{
title: "Second",
id: "second",
"~internalLink": "dUtgloZIckax",
content: trimIndentation`\
<p>
<a class="reference-link" href="#root/dUtgloZIckax">
Backlink text
</a>
</p>
<p>
<a class="reference-link" href="#root/dUtgloZIckax/wsq5D7wgKWrg">
First
</a>
</p>
<p>
<a class="reference-link" href="#root/dUtgloZIckax/TvyONGWYgV7N">
Second
</a>
</p>
`
}
]);
const backlinksResponse = note_map.getBacklinks({
params: {
noteId: note.noteId
}
} as any);
expect(backlinksResponse).toMatchObject([
{
excerpts: [
trimIndentation`\
<div class="ck-content backlink-excerpt"><p>
The quick brownie
</p>
<p>
<a class="reference-link backlink-link" href="#root/dUtgloZIckax">
Backlink text
</a>
</p>
<figure class="image">
${" "}
</figure>
</div>`
],
noteId: "first",
},
{
excerpts: [
trimIndentation`\
<div class="ck-content backlink-excerpt"><p>
<a class="reference-link backlink-link" href="#root/dUtgloZIckax">
Backlink text
</a>
</p>
<p>
<a class="reference-link" href="#root/dUtgloZIckax/wsq5D7wgKWrg">
First
</a>
</p>
<p>
<a class="reference-link" href="#root/dUtgloZIckax/TvyONGWYgV7N">
Second
</a>
</p>
</div>`
],
noteId: "second"
}
]);
});
});

View File

@ -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,25 +241,26 @@ function updateDescendantCountMapForSearch(noteIdToDescendantCountMap: Record<st
}
}
function removeImages(document: Document) {
function removeImages(document: HTMLElement) {
const images = document.getElementsByTagName("img");
while (images && images.length > 0) {
images[0]?.parentNode?.removeChild(images[0]);
for (const image of images) {
image.remove();
}
}
const EXCERPT_CHAR_LIMIT = 200;
type ElementOrText = Element | Text;
type ElementOrText = HTMLElement | TextNode;
function findExcerpts(sourceNote: BNote, referencedNoteId: string) {
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[] = [];
removeImages(document);
for (const linkEl of document.querySelectorAll("a")) {
console.log("Got ", linkEl.innerHTML);
const href = linkEl.getAttribute("href");
if (!href || !href.endsWith(referencedNoteId)) {
@ -270,8 +271,8 @@ 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 +283,7 @@ 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 +291,7 @@ 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 +303,7 @@ 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 +311,7 @@ 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 +328,7 @@ 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");

View File

@ -1,7 +1,88 @@
import { describe, it, expect } from "vitest";
import { renderCode, 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);
expect(result.content).toStrictEqual("<p>Protected note cannot be displayed</p>");
});
describe("Text note", () => {
it("parses simple note", () => {
const content = trimIndentation`\
<figure class="image image-style-align-right image_resized" style="width:29.84%;">
<img style="aspect-ratio:150/150;" src="api/attachments/TnyuBzEXJZln/image/Trilium Demo_icon-color.svg" width="150" height="150">
</figure>
<p>
<strong>
Welcome to Trilium Notes!
</strong>
</p>`;
const note = buildShareNote({ content });
const result = getContent(note);
expect(result.content).toStrictEqual(content);
});
it("handles attachment link", () => {
const content = trimIndentation`\
<h1>Test</h1>
<p>
<a class="reference-link" href="#root/iwTmeWnqBG5Q?viewMode=attachments&amp;attachmentId=q14s2Id7V6pp">
5863845791835102555.mp4
</a>
&nbsp;
</p>
`;
const note = buildShareNote({
content,
attachments: [ { id: "q14s2Id7V6pp", title: "5863845791835102555.mp4" } ]
});
const result = getContent(note);
expect(result.content).toStrictEqual(trimIndentation`\
<h1>Test</h1>
<p>
<a class="reference-link attachment-link role-file" href="api/attachments/q14s2Id7V6pp/download">5863845791835102555.mp4</a>
&nbsp;
</p>
`);
});
it("renders included notes", () => {
buildShareNotes([
{ id: "subnote1", content: `<p>Foo</p><div>Bar</div>` },
{ id: "subnote2", content: `<strong>Baz</strong>` }
]);
const note = buildShareNote({
id: "note1",
content: trimIndentation`\
<p>Before</p>
<section class="include-note" data-note-id="subnote1" data-box-size="small">&nbsp;</section>
<section class="include-note" data-note-id="subnote2" data-box-size="small">&nbsp;</section>
<p>After</p>
`
});
const result = getContent(note);
expect(result.content).toStrictEqual(trimIndentation`\
<p>Before</p>
<p>Foo</p><div>Bar</div>
<strong>Baz</strong>
<p>After</p>
`);
});
});
describe("renderCode", () => {
it("identifies empty content", () => {
const emptyResult: Result = {

View File

@ -1,4 +1,4 @@
import { JSDOM } from "jsdom";
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";
@ -16,7 +16,7 @@ export interface Result {
isEmpty?: boolean;
}
function getContent(note: SNote) {
export function getContent(note: SNote) {
if (note.isProtected) {
return {
header: "",
@ -66,7 +66,8 @@ function renderIndex(result: Result) {
}
function renderText(result: Result, note: SNote) {
const document = new JSDOM(result.content || "").window.document;
if (typeof result.content !== "string") return;
const document = parse(result.content || "");
// Process include notes.
for (const includeNoteEl of document.querySelectorAll("section.include-note")) {
@ -79,11 +80,13 @@ 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);
const includedDocument = parse(includedResult.content).childNodes;
if (includedDocument) {
includeNoteEl.replaceWith(...includedDocument);
}
}
result.isEmpty = document.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")) {
@ -99,7 +102,7 @@ function renderText(result: Result, note: SNote) {
}
}
result.content = document.body.innerHTML;
result.content = document.innerHTML ?? "";
if (result.content.includes(`<span class="math-tex">`)) {
result.header += `
@ -120,7 +123,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 +134,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.appendChild(new TextNode(attachment.title));
} else {
linkEl.removeAttribute("href");
}
@ -164,11 +168,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.appendChild(new TextNode(result.content));
result.content = preEl.outerHTML;
}
}

View File

@ -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 ?? "New note",
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;
}

View File

@ -0,0 +1,135 @@
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";
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<ArrayBufferLike>;
children?: NoteDefinition[];
attachments?: AttachementDefinition[];
isProtected?: boolean;
}
/**
* 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 ?? "New note",
"text",
"text/html",
blobId,
new Date().toUTCString(), // utcDateModified
!!noteDef.isProtected
]);
shaca.notes[note.noteId] = note;
// Handle content
if (noteDef.content) {
note.getContent = () => {
if (noteDef.isProtected) return undefined;
return noteDef.content;
};
}
// 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) {
const childNote = buildShareNote(childDef);
// TODO: Create corresponding SBranch.
}
}
// 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;
}

View File

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

View File

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

74
pnpm-lock.yaml generated
View File

@ -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
@ -449,6 +446,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 +513,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 +678,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 +4803,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==}
@ -9968,6 +9959,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==}
@ -13942,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:
@ -14728,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:
@ -14787,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:
@ -14978,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:
@ -15473,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:
@ -15559,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:
@ -15874,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:
@ -15887,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:
@ -19121,12 +19125,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':
@ -19333,7 +19331,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
@ -21259,7 +21258,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
@ -21514,6 +21513,7 @@ snapshots:
dependencies:
'@asamuzakjp/css-color': 3.1.4
rrweb-cssom: 0.8.0
optional: true
csstype@3.1.3:
optional: true
@ -21728,6 +21728,7 @@ snapshots:
dependencies:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
optional: true
data-view-buffer@1.0.2:
dependencies:
@ -23684,6 +23685,7 @@ snapshots:
html-encoding-sniffer@4.0.0:
dependencies:
whatwg-encoding: 3.1.1
optional: true
html-escaper@2.0.2: {}
@ -24421,6 +24423,7 @@ snapshots:
- bufferutil
- supports-color
- utf-8-validate
optional: true
jsesc@3.1.0: {}
@ -25767,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: {}
@ -27806,7 +27814,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
rrweb-cssom@0.8.0: {}
rrweb-cssom@0.8.0:
optional: true
run-applescript@7.1.0: {}
@ -27982,6 +27991,7 @@ snapshots:
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
optional: true
scheduler@0.19.1:
dependencies:
@ -28875,7 +28885,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
@ -29122,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:
@ -29175,6 +29187,7 @@ snapshots:
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.86
optional: true
tr46@0.0.3: {}
@ -29185,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:
@ -29807,6 +29821,7 @@ snapshots:
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
optional: true
wait-port@1.1.0:
dependencies:
@ -29895,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:
@ -30056,6 +30072,7 @@ snapshots:
dependencies:
tr46: 5.1.1
webidl-conversions: 7.0.0
optional: true
whatwg-url@5.0.0:
dependencies:
@ -30211,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: {}

View File

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

View File

@ -5,7 +5,7 @@
"emitDeclarationOnly": true,
"importHelpers": true,
"isolatedModules": true,
"lib": ["ES2023"],
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "nodenext",
"moduleResolution": "nodenext",
"noEmitOnError": true,