/**
* 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;