diff --git a/src/adapters/non-verbal-resolver.ts b/src/adapters/non-verbal-resolver.ts index dbbf569..e1d60ff 100644 --- a/src/adapters/non-verbal-resolver.ts +++ b/src/adapters/non-verbal-resolver.ts @@ -15,11 +15,13 @@ * exactly one instance per app. */ -import type { NonVerbalEntity, NonVerbalKind } from './non-verbal/types'; +import type { NonVerbalKind } from './non-verbal/types'; +import type { NonVerbalEntity } from 'glossarist'; import { KIND_TO_DIR, KIND_TO_BRIDGE } from './non-verbal/kind'; import { anchorId } from '../utils/non-verbal-anchor'; -export type { NonVerbalEntity, NonVerbalKind } from './non-verbal/types'; +export type { NonVerbalKind } from './non-verbal/types'; +export type { NonVerbalEntity } from 'glossarist'; export interface NonVerbalEntityResolverOptions { basePath?: string; diff --git a/src/adapters/non-verbal/figure-bridge.ts b/src/adapters/non-verbal/figure-bridge.ts index 38c9839..fc31b1e 100644 --- a/src/adapters/non-verbal/figure-bridge.ts +++ b/src/adapters/non-verbal/figure-bridge.ts @@ -23,8 +23,8 @@ * ambiguity with the HTML `` attribute. */ -import type { Figure, FigureImage, FigureImageFormat, FigureImageRole } from './types'; -import { isType, pickField, pickFieldArray, pickFieldRecord, localized } from './prefix'; +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']); @@ -35,18 +35,20 @@ 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') as FigureImageFormat; + const format = FORMAT_SET.has(formatRaw) ? formatRaw : 'svg'; const roleRaw = pickField(raw, 'role'); - const role = roleRaw && ROLE_SET.has(roleRaw) ? (roleRaw as FigureImageRole) : undefined; + const role = roleRaw && ROLE_SET.has(roleRaw) ? roleRaw : undefined; const width = pickField(raw, 'width'); const height = pickField(raw, 'height'); const scale = pickField(raw, 'scale'); - const img: FigureImage = { src, format }; - if (role) img.role = role; - if (typeof width === 'number') img.width = width; - if (typeof height === 'number') img.height = height; - if (typeof scale === 'number') img.scale = scale; - return img; + return new FigureImage({ + src, + format, + ...(role !== undefined && { role }), + ...(typeof width === 'number' && { width }), + ...(typeof height === 'number' && { height }), + ...(typeof scale === 'number' && { scale }), + }); } function imagesFromJsonLd(raw: unknown): FigureImage[] { @@ -85,17 +87,14 @@ export function figureFromJsonLd(doc: Record): Figure | null { const subfigures = subfiguresFromJsonLd(pickField(doc, 'subfigure')); const sources = sourcesFromJsonLd(pickField(doc, 'source')); - const fig: Figure = { kind: 'figure', id, images }; - if (identifier) fig.identifier = identifier; - if (caption) fig.caption = caption; - if (alt) fig.alt = alt; - if (description) fig.description = description; - if (subfigures) fig.subfigures = subfigures; - if (sources.length) fig.sources = sources; - - // pickFieldRecord is unused for figures; keep the import meaningful by - // ensuring no stray image fields leak through (silent no-op). - void pickFieldRecord; - - return fig; + return new Figure({ + id, + images, + ...(identifier && { identifier }), + ...(caption && { caption }), + ...(alt && { alt }), + ...(description && { description }), + ...(subfigures && { subfigures }), + ...(sources.length && { sources }), + }); } diff --git a/src/adapters/non-verbal/formula-bridge.ts b/src/adapters/non-verbal/formula-bridge.ts index e06ccdd..68429f0 100644 --- a/src/adapters/non-verbal/formula-bridge.ts +++ b/src/adapters/non-verbal/formula-bridge.ts @@ -15,7 +15,7 @@ * } */ -import type { Formula, FormulaNotation } from './types'; +import { Formula } from 'glossarist'; import { isType, pickField, localized } from './prefix'; import { sourcesFromJsonLd } from './source-bridge'; @@ -31,18 +31,20 @@ export function formulaFromJsonLd(doc: Record): Formula | null if (!expression) return null; const notationRaw = (pickField(doc, 'notation') ?? '').toLowerCase(); - const notation = NOTATION_SET.has(notationRaw) ? (notationRaw as FormulaNotation) : 'latex'; + const notation = NOTATION_SET.has(notationRaw) ? notationRaw : 'latex'; const identifier = pickField(doc, 'identifier'); const caption = localized(doc, 'caption'); const description = localized(doc, 'description'); const sources = sourcesFromJsonLd(pickField(doc, 'source')); - const f: Formula = { kind: 'formula', id, expression, notation }; - if (identifier) f.identifier = identifier; - if (caption) f.caption = caption; - if (description) f.description = description; - if (sources.length) f.sources = sources; - - return f; + return new Formula({ + id, + expression, + notation, + ...(identifier && { identifier }), + ...(caption && { caption }), + ...(description && { description }), + ...(sources.length && { sources }), + }); } diff --git a/src/adapters/non-verbal/glossarist-augment.d.ts b/src/adapters/non-verbal/glossarist-augment.d.ts new file mode 100644 index 0000000..4ddb714 --- /dev/null +++ b/src/adapters/non-verbal/glossarist-augment.d.ts @@ -0,0 +1,126 @@ +// Local module augmentation for glossarist 0.4.2. +// +// Upstream's published src/models/index.d.ts declares ZERO classes for the +// non-verbal hierarchy (Figure, Table, Formula, FigureImage, NonVerbalEntity, +// SharedNonVerbalEntity, NonVerbalReference + subclasses, BibliographyEntry, +// BibliographyData) plus the localized-string helpers. The top-level +// index.d.ts re-exports the names, so TypeScript silently resolves every +// consumer import to `any`. +// +// This file declares the runtime shape so consumer code can be type-checked. +// DELETE this file when upstream ships proper declarations — tracked by +// PR glossarist/glossarist-js#31 (targets v0.4.3+). + +import type { ConceptSource, GlossaristModel } from 'glossarist'; + +declare module 'glossarist' { + class RegistrableModel extends GlossaristModel { + static register(type: string, cls: typeof RegistrableModel): void; + static fromData(data: Record): RegistrableModel; + } + + class FigureImage extends GlossaristModel { + constructor(data?: { + src?: string | null; + format?: string | null; + role?: string | null; + width?: number | null; + height?: number | null; + scale?: number | null; + }); + readonly src: string | null; + readonly format: string | null; + readonly role: string | null; + readonly width: number | null; + readonly height: number | null; + readonly scale: number | null; + static fromJSON(data: Record): FigureImage; + } + + class NonVerbalEntity extends RegistrableModel { + constructor(data?: Record); + readonly caption: Record | null; + readonly description: Record | null; + readonly alt: Record | null; + readonly sources: ConceptSource[]; + findById(targetId: string): NonVerbalEntity | null; + allIds(): string[]; + static fromJSON(data: Record): NonVerbalEntity; + } + + class SharedNonVerbalEntity extends NonVerbalEntity { + constructor(data?: Record); + readonly id: string | null; + readonly identifier: string | null; + findById(targetId: string): SharedNonVerbalEntity | null; + allIds(): string[]; + static fromJSON(data: Record): SharedNonVerbalEntity; + } + + class Figure extends SharedNonVerbalEntity { + constructor(data?: Record); + readonly images: FigureImage[]; + readonly subfigures: Figure[]; + findById(targetId: string): Figure | null; + allIds(): string[]; + static fromJSON(data: Record): Figure; + } + + class Table extends SharedNonVerbalEntity { + constructor(data?: Record); + readonly content: Record | null; + readonly format: string | null; + static fromJSON(data: Record): Table; + } + + class Formula extends SharedNonVerbalEntity { + constructor(data?: Record); + readonly expression: Record | null; + readonly notation: string | null; + static fromJSON(data: Record): Formula; + } + + const NON_VERBAL_TYPES: readonly string[]; + + class NonVerbalReference extends RegistrableModel { + constructor(data?: Record); + readonly entityId: string | null; + readonly display: string | null; + readonly dedupKey: readonly [string, string | null]; + static fromJSON(data: Record | string): NonVerbalReference; + static register(type: string, cls: typeof NonVerbalReference): void; + } + + class FigureReference extends NonVerbalReference { + static fromJSON(data: Record | string): FigureReference; + } + + class TableReference extends NonVerbalReference { + static fromJSON(data: Record | string): TableReference; + } + + class FormulaReference extends NonVerbalReference { + static fromJSON(data: Record | string): FormulaReference; + } + + class BibliographyEntry extends GlossaristModel { + constructor(data?: Record); + readonly id: string | null; + readonly reference: string | null; + readonly title: string | null; + readonly link: string | null; + readonly type: string | null; + static fromJSON(data: Record): BibliographyEntry; + } + + class BibliographyData extends GlossaristModel { + constructor(data?: Record); + readonly entries: BibliographyEntry[]; + find(id: string): BibliographyEntry | null; + readonly keys: string[]; + toYAML(): string; + toJSON(): { bibliography: BibliographyEntry[] }; + static fromYAML(yamlString: string): BibliographyData; + static fromJSON(data: Record): BibliographyData; + } +} diff --git a/src/adapters/non-verbal/index.ts b/src/adapters/non-verbal/index.ts index 71b917d..c81634e 100644 --- a/src/adapters/non-verbal/index.ts +++ b/src/adapters/non-verbal/index.ts @@ -1,33 +1,36 @@ /** * Public API for the non-verbal entity model layer. * - * Re-exports the types, bridges, and dispatch table. Components and - * composables import from here — never from individual files — so the - * internal layout can evolve without breaking the public surface. + * Model classes (Figure, Table, Formula, FigureImage, NonVerbalEntity) are + * re-exported from `glossarist` — upstream is the SSOT for the model. + * Consumer-owned types live in `./types`. */ export type { LocalizedString, NonVerbalKind, - FigureImage, FigureImageFormat, FigureImageRole, NonVerbalSource, NonVerbalSourceOrigin, NonVerbalSourceRef, NonVerbalSourceLocality, - NonVerbalEntityBase, - Figure, - Table, TableContent, TableFormat, - Formula, FormulaNotation, - NonVerbalEntity, NonVerbRepV3, NonVerbalReference, } from './types'; +export type { + Figure, + FigureImage, + Table, + Formula, + NonVerbalEntity, + SharedNonVerbalEntity, +} from 'glossarist'; + export { figureFromJsonLd } from './figure-bridge'; export { tableFromJsonLd } from './table-bridge'; export { formulaFromJsonLd } from './formula-bridge'; diff --git a/src/adapters/non-verbal/kind.ts b/src/adapters/non-verbal/kind.ts index d95d3f0..a5c7c5e 100644 --- a/src/adapters/non-verbal/kind.ts +++ b/src/adapters/non-verbal/kind.ts @@ -1,4 +1,5 @@ -import type { NonVerbalEntity, NonVerbalKind } from './types'; +import type { NonVerbalKind } from './types'; +import type { NonVerbalEntity } from 'glossarist'; import { ENTITY_DIRECTORIES, ENTITY_TYPES, diff --git a/src/adapters/non-verbal/table-bridge.ts b/src/adapters/non-verbal/table-bridge.ts index dd56fc2..a360eb9 100644 --- a/src/adapters/non-verbal/table-bridge.ts +++ b/src/adapters/non-verbal/table-bridge.ts @@ -20,7 +20,8 @@ * } */ -import type { Table, TableContent, TableFormat } from './types'; +import { Table } from 'glossarist'; +import type { TableContent } from './types'; import { isType, pickField, localized } from './prefix'; import { sourcesFromJsonLd } from './source-bridge'; @@ -83,16 +84,17 @@ export function tableFromJsonLd(doc: Record): Table | null { if (!content) return null; const formatRaw = (pickField(doc, 'format') ?? '').toLowerCase(); - const format = FORMAT_SET.has(formatRaw) ? (formatRaw as TableFormat) : undefined; + const format = FORMAT_SET.has(formatRaw) ? formatRaw : undefined; const sources = sourcesFromJsonLd(pickField(doc, 'source')); - const t: Table = { kind: 'table', id, content }; - if (identifier) t.identifier = identifier; - if (caption) t.caption = caption; - if (description) t.description = description; - if (format) t.format = format; - if (sources.length) t.sources = sources; - - return t; + return new Table({ + id, + content: content as Record, + ...(identifier && { identifier }), + ...(caption && { caption }), + ...(description && { description }), + ...(format && { format }), + ...(sources.length && { sources }), + }); } diff --git a/src/adapters/non-verbal/types.ts b/src/adapters/non-verbal/types.ts index 235218a..f5b6568 100644 --- a/src/adapters/non-verbal/types.ts +++ b/src/adapters/non-verbal/types.ts @@ -1,18 +1,23 @@ /** - * Non-verbal entity model — TypeScript projection of the authoritative - * glossarist-ruby model. + * Consumer-side types for non-verbal entities. * - * The authoritative model lives in glossarist-ruby (Figure, Table, Formula - * inherit from NonVerbalEntity). This file mirrors that model in TypeScript - * for the consumer side. It does not redefine the model — every field here - * corresponds to a field in the authoritative source. + * Model classes (Figure, Table, Formula, FigureImage, NonVerbalEntity) are + * imported from `glossarist` — the upstream library is the SSOT for the + * model. This file holds only what is genuinely consumer-owned: * - * See: - * ../glossarist-ruby/lib/glossarist/non_verbal_entity.rb - * ../glossarist-ruby/lib/glossarist/figure.rb - * ../glossarist-ruby/lib/glossarist/table.rb - * ../glossarist-ruby/lib/glossarist/formula.rb - * ../glossarist-ruby/lib/glossarist/figure_image.rb + * - `NonVerbalKind`: routing discriminator used by the resolver, the + * anchor scheme, the mention dispatcher, and the section router. + * - `NonVerbRepV3`: local view of NonVerbRep's V3 shape. Upstream's + * published .d.ts still describes the pre-V3 `ref`/`text` shape; this + * interface lets the consumer type-check against runtime reality. + * Delete when upstream ships a corrected declaration. + * - `NonVerbalReference`: consumer-side view of inline mentions like + * `{{fig:foo}}`. Carries a `kind` for UI routing. + * - `LocalizedString`, `FigureImageFormat`, `FigureImageRole`, + * `TableFormat`, `TableContent`, `FormulaNotation`: string-union + * refinements the consumer validates at bridge time. + * - `NonVerbalSource*`: wire shape for JSON-LD source entries. Stays + * consumer-side until upstream ships a V3 NonVerbalSource model. */ export type LocalizedString = Record; @@ -23,15 +28,6 @@ export type FigureImageFormat = 'svg' | 'png' | 'jpg' | 'jpeg' | 'gif' | 'webp' export type FigureImageRole = 'vector' | 'raster' | 'dark' | 'light' | 'print'; -export interface FigureImage { - src: string; - format: FigureImageFormat; - role?: FigureImageRole; - width?: number; - height?: number; - scale?: number; -} - export interface NonVerbalSourceRef { source?: string; id?: string; @@ -62,56 +58,42 @@ export interface NonVerbalSource { origin?: NonVerbalSourceOrigin; } -export interface NonVerbalEntityBase { - id: string; - identifier?: string; - caption?: LocalizedString; - description?: LocalizedString; - alt?: LocalizedString; - sources?: NonVerbalSource[]; -} - -export interface Figure extends NonVerbalEntityBase { - kind: 'figure'; - images: FigureImage[]; - subfigures?: Figure[]; -} - export type TableFormat = 'html' | 'markdown' | 'asciidoc'; export type TableContent = | { kind: 'structured'; headers: LocalizedString[]; rows: LocalizedString[][] } | { kind: 'markup'; markup: LocalizedString }; -export interface Table extends NonVerbalEntityBase { - kind: 'table'; - content: TableContent; - format?: TableFormat; -} - export type FormulaNotation = 'latex' | 'mathml' | 'asciimath'; -export interface Formula extends NonVerbalEntityBase { - kind: 'formula'; - expression: LocalizedString; - notation: FormulaNotation; -} - -export type NonVerbalEntity = Figure | Table | Formula; - /** * V3 NonVerbRep runtime shape. * * glossarist-js's runtime `NonVerbRep` (post-V3 reshape) holds the same - * localized fields as `NonVerbalEntityBase` plus a `type` discriminator + * localized fields as the base NonVerbalEntity plus a `type` discriminator * and an `images[]` array. The published `.d.ts` (still stale at v0.4.2) * describes the pre-V3 `ref`/`text` shape; this local interface lets * consumer code type-check against reality. Drop when upstream ships a - * corrected `.d.ts`. + * corrected `.d.ts` (glossarist/glossarist-js#31). */ -export interface NonVerbRepV3 extends NonVerbalEntityBase { +export interface NonVerbRepV3 { + id: string; + identifier?: string | null; type: string | null; - images: FigureImage[]; + caption?: LocalizedString | null; + description?: LocalizedString | null; + alt?: LocalizedString | null; + images: NonVerbRepImage[]; + sources?: NonVerbalSource[]; +} + +export interface NonVerbRepImage { + src: string; + format?: string | null; + role?: string | null; + width?: number | null; + height?: number | null; + scale?: number | null; } export interface NonVerbalReference { diff --git a/src/components/NonVerbalRepDisplay.vue b/src/components/NonVerbalRepDisplay.vue index 642bc03..dd886f3 100644 --- a/src/components/NonVerbalRepDisplay.vue +++ b/src/components/NonVerbalRepDisplay.vue @@ -1,7 +1,7 @@