/**
* Figure bridge — JSON-LD → Figure (TS model).
*
* Wire format (per task 11 + glossarist-ruby's planned export):
*
* {
* "@id": "https://glossarist.org/{ds}/figure/{id}",
* "@type": "gl:Figure" | "gloss:Figure",
* "gl:id": "{id}",
* "gl:identifier": "Figure 7c", // plain string
* "gl:caption": { "eng": "...", "fra": "..." },
* "gl:altText": { "eng": "..." }, // mapped to model.alt
* "gl:description": { "eng": "..." },
* "gl:image": [
* { "gl:src": "x.png", "gl:format": "png", "gl:role": "raster",
* "gl:width": 1600, "gl:height": 1200, "gl:scale": 1 }
* ],
* "gl:subfigure": [ ... recursive Figure docs ... ],
* "gl:source": [ ... NonVerbalSource docs ... ]
* }
*
* The wire field `gl:altText` is mapped to the model field `alt` to avoid
* ambiguity with the HTML `
` attribute.
*/
import { Figure, FigureImage } from 'glossarist';
import { isType, pickField, pickFieldArray, localized } from './prefix';
import { sourcesFromJsonLd } from './source-bridge';
const FORMAT_SET: ReadonlySet = new Set(['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif']);
const ROLE_SET: ReadonlySet = new Set(['vector', 'raster', 'dark', 'light', 'print']);
function imageFromJsonLd(raw: Record): FigureImage | null {
const src = pickField(raw, 'src');
if (!src) return null;
const formatRaw = (pickField(raw, 'format') ?? '').toLowerCase();
const format = FORMAT_SET.has(formatRaw) ? formatRaw : 'svg';
const roleRaw = pickField(raw, 'role');
const role = roleRaw && ROLE_SET.has(roleRaw) ? roleRaw : undefined;
const width = pickField(raw, 'width');
const height = pickField(raw, 'height');
const scale = pickField(raw, 'scale');
return new FigureImage({
src,
format,
...(role !== undefined && { role }),
...(typeof width === 'number' && { width }),
...(typeof height === 'number' && { height }),
...(typeof scale === 'number' && { scale }),
} as any);
}
function imagesFromJsonLd(raw: unknown): FigureImage[] {
if (!Array.isArray(raw)) return [];
const out: FigureImage[] = [];
for (const entry of raw) {
if (!entry || typeof entry !== 'object') continue;
const img = imageFromJsonLd(entry as Record);
if (img) out.push(img);
}
return out;
}
function subfiguresFromJsonLd(raw: unknown): Figure[] | undefined {
if (!Array.isArray(raw)) return undefined;
const out: Figure[] = [];
for (const entry of raw) {
if (!entry || typeof entry !== 'object') continue;
const sub = figureFromJsonLd(entry as Record);
if (sub) out.push(sub);
}
return out.length ? out : undefined;
}
export function figureFromJsonLd(doc: Record): Figure | null {
if (!isType(doc, 'Figure')) return null;
const id = pickField(doc, 'id') ?? '';
if (!id) return null;
const identifier = pickField(doc, 'identifier');
const caption = localized(doc, 'caption');
const alt = localized(doc, 'altText');
const description = localized(doc, 'description');
const images = imagesFromJsonLd(pickFieldArray(doc, 'image'));
const subfigures = subfiguresFromJsonLd(pickField(doc, 'subfigure'));
const sources = sourcesFromJsonLd(pickField(doc, 'source'));
return new Figure({
id,
images,
...(identifier && { identifier }),
...(caption && { caption }),
...(alt && { alt }),
...(description && { description }),
...(subfigures && { subfigures }),
...(sources.length && { sources }),
} as any);
}