import { __cfHelpers } from "commonfabric"; import { computed, equals, lift, NAME, pattern, UI, type VNode, Writable, } from "commonfabric"; /** * Type for pieces used in the mentionable/backlinks system. * * Note: While `mentioned` and `backlinks` are typed as required for structural * compatibility, at runtime most pieces don't actually have these fields. * Only patterns like notes and calendar events define them. The computeIndex * function safely handles pieces that lack these fields. */ export type MentionablePiece = { [NAME]?: string; isHidden?: boolean; isMentionable?: boolean; mentioned?: MentionablePiece[]; backlinks?: MentionablePiece[]; mentionable?: MentionablePiece[] | { get?: () => MentionablePiece[] }; }; export type MentionableCell = Writable; export type WritableBacklinks = { mentioned?: MentionableCell[]; backlinks?: Writable; }; type Input = { allPieces: MentionableCell[]; }; export type Output = { mentionable: MentionableCell[]; }; const computeIndex = lift<{ allPieces: MentionableCell[] | undefined }, void>( ({ allPieces }) => { const cs = allPieces ?? []; // Reset backlinks for pieces that support it. // Many pieces don't have backlinks (e.g., auth pieces, google patterns), // so we safely skip them with optional chaining. // Also skip undefined/null entries that may exist in the array. for (const c of cs) { if (!c) continue; const value = c.get(); if (!value || !("backlinks" in value)) continue; c.key("backlinks").set([]); } // Populate backlinks from mentioned references. // Again, use optional chaining since not all pieces support backlinks. for (const c of cs) { if (!c) continue; const mentions = c.key("mentioned").get() ?? []; for (let i = 0; i < mentions.length; i++) { const m = c.key("mentioned").key(i); const target = m?.get(); if (!target || !("backlinks" in target)) continue; m.key("backlinks").push(c); } } }, ); /** * BacklinksIndex builds a map of backlinks across all pieces and exposes a * unified mentionable list for consumers like editors. * * Behavior: * - Backlinks are computed by scanning each piece's `mentioned` list and * mapping mention target -> list of source pieces. * - Mentionable list is a union of: * - every piece in `allPieces` * - any items a piece exports via a `mentionable` property * (either an array of pieces or a Cell of such an array) * * The backlinks map is keyed by a piece's `content` value (falling back to * its `[NAME]`). This mirrors how existing note patterns identify notes when * computing backlinks locally. */ const MAX_MENTIONABLE_DEPTH = 5; const computeMentionable = lift< { allPieces: MentionableCell[] }, MentionableCell[] >(({ allPieces: pieceList }) => { const cs = pieceList ?? []; const out: MentionableCell[] = []; function isVisited(piece: MentionableCell): boolean { return out.some((other) => equals(piece, other)); } function collect(piece: MentionableCell, depth: number) { if (!piece) return; const value = piece.get(); if (!value || value.isMentionable === false) return; if (isVisited(piece)) return; out.push(piece); if (depth >= MAX_MENTIONABLE_DEPTH) return; const mentionable = piece.key("mentionable"); const items = mentionable.get() ?? []; if (Array.isArray(items)) { for (let i = 0; i < items.length; i++) { collect(mentionable.key(i), depth + 1); } } } for (const c of cs) { collect(c, 0); } return out; }); type Entry = { piece: MentionableCell; name: string; backlinks: MentionableCell[]; }; /** Sub-pattern to render a single entry row with its backlinks. */ const EntryRow = pattern(({ piece, backlinks }) => { return { [UI]: ( {backlinks.map((link) => ( ))} ), }; }); const BacklinksIndex = pattern(({ allPieces }) => { const allPieceCells = allPieces as MentionableCell[]; computeIndex({ allPieces: allPieceCells }); // Compute mentionable list from allPieces reactively const mentionable = computeMentionable({ allPieces: allPieceCells, }) as MentionableCell[]; const query = new Writable(""); // Build resolved entries with backlinks materialized as plain arrays const entries = computed(() => { const items = Array.isArray(mentionable) ? mentionable : []; const result: Entry[] = []; for (const piece of items) { if (!piece) continue; const value = piece.get(); const name = (value?.[NAME] ?? "").toString(); const backlinks = piece.key("backlinks"); const bl = backlinks.get() ?? []; result.push({ piece, name, backlinks: Array.isArray(bl) ? bl.map((_, i) => backlinks.key(i)) : [], }); } return result; }); // Filter by name const filtered = computed(() => { const q = query.get().toLowerCase().trim(); if (!q) return entries; return entries.filter((e) => e.name.toLowerCase().includes(q)); }); const totalCount = computed(() => entries.length); const filteredCount = computed(() => filtered.length); return { [NAME]: "BacklinksIndex", [UI]: (

Mentions

{filteredCount} of {totalCount} pieces {filtered.map((entry) => ( ))}
), mentionable, }; }); export default BacklinksIndex; // @ts-ignore: Internals function h(...args: any[]) { return __cfHelpers.h.apply(null, args); }