/** * Render-options composable — builds resolvers for the content renderer. * * The citeResolver is the load-bearing addition: it connects inline * {{cite:sourceId}} mentions to the existing ReferenceResolver cascade * (factory.resolver.resolveCite). This closes the data/deployment * boundary: the data author writes {{cite:iev_702-02-07}}, the resolver * walks the deployment's sourceRefs → routing → link cascade, and the * renderer emits the correct HTML based on classification. */ import { ref, type Ref } from 'vue'; import type { RenderOptions, BibResolver, CiteResolver, NonVerbalRefResolver } from '../utils/content-renderer'; import type { NonVerbalKind } from '../adapters/non-verbal/types'; import { getFactory } from '../adapters/factory'; import { anchorId } from '../utils/non-verbal-anchor'; import { escapeAttr, escapeHtml } from '../utils/escape'; /** Minimal shape of a ConceptSource that citeResolver needs. */ interface ConceptSourceLike { id?: string | null; origin?: { ref?: { source?: string | null; id?: string | null; version?: string | null } | null; locality?: { type?: string | null; reference_from?: string | null; referenceTo?: string | null; reference_to?: string | null; referenceFrom?: string | null } | null; link?: string | null; } | null; } export function useRenderOptions( registerId: () => string, getSources?: () => readonly ConceptSourceLike[] | undefined, ) { const ready = ref(false); async function ensureBibLoaded() { const id = registerId(); if (!id) return; await getFactory().bibliography(id).load(); ready.value = true; } const bibResolver: BibResolver = (refId, title) => { const id = registerId(); const entry = id ? getFactory().bibliography(id).findById(refId) : null; if (!entry) { return `${escapeHtml(title)}`; } const display = title || entry.reference || refId; if (entry.link) { return `${escapeHtml(display)}`; } return `${escapeHtml(display)}`; }; /** * citeResolver — the canonical path for {{cite:sourceId}} mentions. * * Looks up the concept's own sources[] for a matching id, then * delegates to ReferenceResolver.resolveCite() which walks the * deployment's cascade (sourceRefs → routing → link → unresolved). */ const citeResolver: CiteResolver = (key, label) => { const rid = registerId(); const sources = getSources?.(); if (!sources || !rid) { return `${escapeHtml(label ?? key)}`; } // Parse DATASET:ID format. {{cite:IEV:702-02-07}} → dataset="IEV", idPart="702-02-07" // Match against: raw id, id part, or origin.ref { source: dataset, id: idPart } const lastColon = key.lastIndexOf(':'); const idPart = lastColon > 0 ? key.slice(lastColon + 1) : key; const datasetPart = lastColon > 0 ? key.slice(0, lastColon) : ''; const source = sources.find(s => { // Match by source id (raw or id part) if (s.id === key || s.id === idPart) return true; // Match by origin.ref const ref = s.origin?.ref; if (ref) { if (datasetPart && ref.source === datasetPart && ref.id === idPart) return true; if (ref.id === key || ref.id === idPart) return true; } return false; }); if (!source?.origin) { return `${escapeHtml(label ?? key)}`; } const factory = getFactory(); const resolution = factory.resolver.resolveCite(source.origin, rid); const refId = source.origin.ref?.id ?? key; const refSource = source.origin.ref?.source ?? ''; const display = label ?? refId; switch (resolution.classification) { case 'internal-citation': { if (!resolution.resolved) break; const { registerId: targetReg, conceptId } = resolution.resolved; const isCross = targetReg !== rid; const crossBadge = isCross ? '' : ''; return `${escapeHtml(display)} ${crossBadge}`; } case 'self-contained-citation': { const link = source.origin.link; if (link) { return `${escapeHtml(refSource || display)} `; } return `${escapeHtml(refSource || display)}`; } case 'external-citation': { const link = source.origin.link; if (link) { return `${escapeHtml(refSource || display)} `; } return `${escapeHtml(refSource || display)}`; } } return `${escapeHtml(display)}`; }; const nonVerbalRefResolver: NonVerbalRefResolver = (kind: NonVerbalKind, entityId, display) => { const id = registerId(); if (!id) { const label = display ?? entityId; return `${escapeHtml(label)}`; } const href = `#${anchorId(kind, id, entityId)}`; const label = display ?? defaultLabelForKind(kind, entityId); const cls = `nv-ref nv-ref--${kind}`; return `${escapeHtml(label)}`; }; return { ready, ensureBibLoaded, bibResolver, citeResolver, nonVerbalRefResolver }; } function defaultLabelForKind(kind: NonVerbalKind, entityId: string): string { switch (kind) { case 'figure': return `Figure ${entityId}`; case 'table': return `Table ${entityId}`; case 'formula': return `Formula ${entityId}`; } } /** Ensure `ready` is tracked as a Ref for consumers that read `.value`. */ export type RenderOptionsReady = Ref;