diff --git a/apps/client/src/widgets/dialogs/add_link.tsx b/apps/client/src/widgets/dialogs/add_link.tsx index 2206bd9dd1..552ea07c0c 100644 --- a/apps/client/src/widgets/dialogs/add_link.tsx +++ b/apps/client/src/widgets/dialogs/add_link.tsx @@ -14,6 +14,25 @@ import { useTriliumEvent } from "../react/hooks"; type LinkType = "reference-link" | "external-link" | "hyper-link"; +function findAnchorIds(content: string): string[] { + const re = /]*)>(<\/a>)?/g; + const ids: string[] = []; + let match; + + while ((match = re.exec(content))) { + const attrs = match[1]; + if (/\bhref\s*=/.test(attrs)) continue; + + const idMatch = /\bid\s*=\s*"([^"]+)"/.exec(attrs) ?? /\bid\s*=\s*'([^']+)'/.exec(attrs); + if (!idMatch) continue; + + const id = idMatch[1]; + if (!ids.includes(id)) ids.push(id); + } + + return ids; +} + export interface AddLinkOpts { text: string; hasSelection: boolean; @@ -67,12 +86,25 @@ export default function AddLinkDialog() { const noteId = tree.getNoteIdFromUrl(suggestion.notePath); if (noteId) { setDefaultLinkTitle(noteId); - froca.getNote(noteId).then((note) => { - if (cancelled) return; - const bkms = note?.getLabels("internalBookmark").map((l) => l.value) ?? []; + (async () => { + const note = await froca.getNote(noteId); + if (cancelled || !note) return; + + let bkms = note.getLabels("internalBookmark").map((l) => l.value); + + // Fall back to scanning the note content for anchors if no labels + // are present (e.g. notes that predate the bookmark-label feature). + if (bkms.length === 0 && note.type === "text") { + const content = await note.getContent(); + if (cancelled) return; + if (typeof content === "string") { + bkms = findAnchorIds(content); + } + } + setBookmarks(bkms); setSelectedBookmark(""); - }); + })(); } resetExternalLink(); } diff --git a/packages/trilium-core/src/becca/becca-interface.ts b/packages/trilium-core/src/becca/becca-interface.ts index e29c7d63c9..249030ccfd 100644 --- a/packages/trilium-core/src/becca/becca-interface.ts +++ b/packages/trilium-core/src/becca/becca-interface.ts @@ -279,8 +279,10 @@ export default class Becca { */ getFlatTextIndex(): { notes: BNote[], flatTexts: string[], noteIdToIdx: Map } { if (!this.flatTextIndex) { - // Measure heap before building - const heapBefore = process.memoryUsage().heapUsed; + // Measure heap before building (only available under Node.js) + const heapBefore = typeof process !== "undefined" && typeof process.memoryUsage === "function" + ? process.memoryUsage().heapUsed + : null; const allNoteSet = this.getAllNoteSet(); const notes: BNote[] = []; @@ -296,10 +298,12 @@ export default class Becca { this.flatTextIndex = { notes, flatTexts, noteIdToIdx }; this.dirtyFlatTextNoteIds.clear(); - // Measure heap after building and log - const heapAfter = process.memoryUsage().heapUsed; - const heapDelta = heapAfter - heapBefore; - getLog().info(`Flat text search index built: ${notes.length} notes, ${formatSize(heapDelta)}`); + if (heapBefore !== null) { + const heapDelta = process.memoryUsage().heapUsed - heapBefore; + getLog().info(`Flat text search index built: ${notes.length} notes, ${formatSize(heapDelta)}`); + } else { + getLog().info(`Flat text search index built: ${notes.length} notes`); + } } else if (this.dirtyFlatTextNoteIds.size > 0) { // Incremental update: only recompute flat texts for dirtied notes const { flatTexts, noteIdToIdx } = this.flatTextIndex; diff --git a/packages/trilium-core/src/becca/entities/battribute.ts b/packages/trilium-core/src/becca/entities/battribute.ts index ac8e3e2553..ee90b0e48a 100644 --- a/packages/trilium-core/src/becca/entities/battribute.ts +++ b/packages/trilium-core/src/becca/entities/battribute.ts @@ -118,7 +118,15 @@ class BAttribute extends AbstractBeccaEntity { } isAutoLink() { - return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name); + if (this.type === "relation") { + return ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name); + } + + if (this.type === "label") { + return this.name === "internalBookmark"; + } + + return false; } get note() { diff --git a/packages/trilium-core/src/services/notes.ts b/packages/trilium-core/src/services/notes.ts index 03215c5b91..2ecd711f5e 100644 --- a/packages/trilium-core/src/services/notes.ts +++ b/packages/trilium-core/src/services/notes.ts @@ -515,19 +515,27 @@ function findIncludeNoteLinks(content: string, foundLinks: FoundLink[]) { /** * Extracts bookmark IDs from CKEditor bookmark anchors (`` without href). * Bookmarks are stored as labels on the note so they can be looked up without parsing content. + * Matches id regardless of attribute order; skips anchors with href (those are regular links). */ export function findBookmarks(content: string): string[] { - const re = /]*>(<\/a>)?/g; + const re = /]*)>(<\/a>)?/g; const bookmarks: string[] = []; let match; while ((match = re.exec(content))) { + const attrs = match[1]; + // Skip anchors that also have an href (those are regular links, not bookmarks) - if (match[0].includes("href=")) { + if (/\bhref\s*=/.test(attrs)) { continue; } - const id = match[1]; + const idMatch = /\bid\s*=\s*"([^"]+)"/.exec(attrs) ?? /\bid\s*=\s*'([^']+)'/.exec(attrs); + if (!idMatch) { + continue; + } + + const id = idMatch[1]; if (!bookmarks.includes(id)) { bookmarks.push(id); }