chore(standalone): fix typecheck

This commit is contained in:
Elian Doran
2026-04-19 20:43:27 +03:00
parent 28c6826b35
commit d97818a594
9 changed files with 88 additions and 20 deletions

View File

@@ -1,2 +1,3 @@
import { note_service } from "@triliumnext/core";
export { findBookmarks } from "@triliumnext/core";
export default note_service;

View File

@@ -12,15 +12,12 @@
* - These tests focus on the in-memory/CPU-bound parts of the pipeline
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import searchService from "./search.js";
import BNote from "../../../becca/entities/bnote.js";
import BBranch from "../../../becca/entities/bbranch.js";
import SearchContext from "../search_context.js";
import becca from "../../../becca/becca.js";
import beccaService from "../../../becca/becca_service.js";
import { NoteBuilder, note, id } from "../../../test/becca_mocking.js";
import SearchResult from "../search_result.js";
import { normalizeSearchText } from "../utils/text_utils.js";
import { becca, becca_service as beccaService, BNote, BBranch, search as searchService, SearchContext, becca_mocking } from "@triliumnext/core";
import SearchResult from "@triliumnext/core/src/services/search/search_result.js";
import { normalizeSearchText } from "@triliumnext/core/src/services/search/utils/text_utils.js";
const { NoteBuilder, note } = becca_mocking;
type NoteBuilder = InstanceType<typeof NoteBuilder>;
// ── helpers ──────────────────────────────────────────────────────────

View File

@@ -1,2 +1 @@
import { becca_mocking } from "@triliumnext/core";
export const NoteBuilder = becca_mocking.NoteBuilder;
export { NoteBuilder, note, id, findNoteByTitle } from "@triliumnext/core/src/test/becca_mocking.js";

View File

@@ -5,7 +5,7 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
import dateUtils from "../../services/utils/date";
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
import type { AttributeRow, AttributeType } from "@triliumnext/commons";
import { sanitizeAttributeName } from "../../services/utils/index.js";
import { normalize, sanitizeAttributeName } from "../../services/utils/index.js";
interface SavingOpts {
skipValidation?: boolean;
@@ -34,6 +34,11 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
value!: string;
isInheritable!: boolean;
/** Pre-normalized (lowercase, diacritics removed) name for search. */
normalizedName!: string;
/** Pre-normalized (lowercase, diacritics removed) value for search. */
normalizedValue!: string;
constructor(row?: AttributeRow) {
super();
@@ -59,6 +64,10 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
this.isInheritable = !!isInheritable;
this.utcDateModified = utcDateModified;
// Pre-compute normalized forms for search (avoids repeated normalize() calls in hot loops)
this.normalizedName = normalize(this.name);
this.normalizedValue = normalize(this.value);
return this;
}
@@ -194,6 +203,11 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
super.beforeSaving();
// Recompute normalized fields in case name/value were modified directly
// (e.g., attr.value = "..." followed by attr.save())
this.normalizedName = normalize(this.name);
this.normalizedValue = normalize(this.value);
this.becca.attributes[this.attributeId] = this;
}

View File

@@ -90,7 +90,7 @@ export { default as SearchContext } from "./services/search/search_context";
export { default as search, } from "./services/search/services/search";
export { type default as SearchResult } from "./services/search/search_result";
export { type SearchParams } from "./services/search/services/types";
export { default as note_service } from "./services/notes";
export { default as note_service, findBookmarks } from "./services/notes";
export type { NoteParams } from "./services/notes";
export * as sanitize from "./services/sanitizer";
export * as routes from "./routes";

View File

@@ -5,7 +5,7 @@ import { beforeAll, describe, expect, it } from "vitest";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import cls from "../cls.js";
import { getContext } from "../context.js";
import sql_init from "../sql_init.js";
import TaskContext from "../task_context.js";
import enex from "./enex.js";
@@ -17,7 +17,7 @@ async function testImport(fileName: string) {
const taskContext = TaskContext.getInstance("import-enex", "importNotes", {});
return new Promise<{ importedNote: BNote; rootNote: BNote }>((resolve, reject) => {
cls.init(async () => {
getContext().init(async () => {
const rootNote = becca.getNote("root");
if (!rootNote) {
expect(rootNote).toBeTruthy();

View File

@@ -512,6 +512,54 @@ function findIncludeNoteLinks(content: string, foundLinks: FoundLink[]) {
return content;
}
/**
* Extracts bookmark IDs from CKEditor bookmark anchors (`<a id="..."></a>` without href).
* Bookmarks are stored as labels on the note so they can be looked up without parsing content.
*/
export function findBookmarks(content: string): string[] {
const re = /<a\s+id="([^"]+)"[^>]*>(<\/a>)?/g;
const bookmarks: string[] = [];
let match;
while ((match = re.exec(content))) {
// Skip anchors that also have an href (those are regular links, not bookmarks)
if (match[0].includes("href=")) {
continue;
}
const id = match[1];
if (!bookmarks.includes(id)) {
bookmarks.push(id);
}
}
return bookmarks;
}
function saveBookmarks(note: BNote, content: string) {
const foundBookmarks = findBookmarks(content);
const existingBookmarks = note.getOwnedLabels("internalBookmark");
for (const bookmarkId of foundBookmarks) {
const existing = existingBookmarks.find((l) => l.value === bookmarkId);
if (!existing) {
new BAttribute({
noteId: note.noteId,
type: "label",
name: "internalBookmark",
value: bookmarkId
}).save();
}
}
// Remove bookmarks that are no longer in the content
const unusedBookmarks = existingBookmarks.filter((l) => !foundBookmarks.includes(l.value));
for (const unused of unusedBookmarks) {
unused.markAsDeleted();
}
}
function findRelationMapLinks(content: string, foundLinks: FoundLink[]) {
try {
const obj = JSON.parse(content);
@@ -727,6 +775,7 @@ function saveLinks(note: BNote, content: string | Uint8Array) {
content = findImageLinks(content, foundLinks);
content = findInternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks);
saveBookmarks(note, content);
({ forceFrontendReload, content } = checkImageAttachments(note, content));
} else if (note.type === "relationMap" && typeof content === "string") {

View File

@@ -54,22 +54,22 @@ describe("sanitize", () => {
describe("bookmark anchors", () => {
it("preserves id attribute on empty <a> tags (CKEditor bookmarks)", () => {
const dirty = `<a id="my-bookmark"></a>`;
expect(html_sanitizer.sanitize(dirty)).toBe(dirty);
expect(sanitizeHtml(dirty)).toBe(dirty);
});
it("preserves id attribute on <a> tags with bookmark class", () => {
const dirty = `<a id="chapter-1" class="ck-bookmark"></a>`;
expect(html_sanitizer.sanitize(dirty)).toBe(dirty);
expect(sanitizeHtml(dirty)).toBe(dirty);
});
it("strips id attribute from non-anchor tags to prevent DOM clobbering", () => {
const dirty = `<div id="loginForm">content</div>`;
expect(html_sanitizer.sanitize(dirty)).toBe(`<div>content</div>`);
expect(sanitizeHtml(dirty)).toBe(`<div>content</div>`);
});
it("strips id attribute from <img> tags to prevent DOM clobbering", () => {
const dirty = `<img id="someId" src="test.png" />`;
expect(html_sanitizer.sanitize(dirty)).toBe(`<img src="test.png" />`);
expect(sanitizeHtml(dirty)).toBe(`<img src="test.png" />`);
});
});
});

View File

@@ -1,6 +1,7 @@
"use strict";
import hoistedNoteService from "../hoisted_note.js";
import optionService from "../options.js";
import type { SearchParams } from "./services/types.js";
class SearchContext {
@@ -19,6 +20,8 @@ class SearchContext {
debugInfo: {} | null;
fuzzyAttributeSearch: boolean;
enableFuzzyMatching: boolean; // Controls whether fuzzy matching is enabled for this search phase
/** When true, skip the two-phase fuzzy fallback and use the single-token fast path. */
autocomplete: boolean;
highlightedTokens: string[];
originalQuery: string;
fulltextQuery: string;
@@ -46,7 +49,12 @@ class SearchContext {
this.debug = params.debug;
this.debugInfo = null;
this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch;
this.enableFuzzyMatching = true; // Default to true for backward compatibility
this.autocomplete = !!params.autocomplete;
try {
this.enableFuzzyMatching = optionService.getOptionBool("searchEnableFuzzyMatching");
} catch {
this.enableFuzzyMatching = true; // Default to true if option not yet initialized
}
this.highlightedTokens = [];
this.originalQuery = "";
this.fulltextQuery = ""; // complete fulltext part