glossarist 0.3.8 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glossarist",
3
- "version": "0.3.8",
3
+ "version": "0.4.1",
4
4
  "description": "JavaScript SDK for Glossarist GCR packages — read, write, validate, and manage terminology concepts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -86,22 +86,14 @@ export class ConceptCollection {
86
86
  for (const t of lc.terms) {
87
87
  if ((t.designation ?? '').toLowerCase().includes(q)) return true;
88
88
  }
89
- if (this._searchDefs(lc.definitions, q)) return true;
90
- if (this._searchDefs(lc.notes, q)) return true;
91
- if (this._searchDefs(lc.examples, q)) return true;
92
- if (this._searchDefs(lc.annotations, q)) return true;
89
+ }
90
+ for (const { text } of c.walkTexts()) {
91
+ if (text.toLowerCase().includes(q)) return true;
93
92
  }
94
93
  return false;
95
94
  }));
96
95
  }
97
96
 
98
- _searchDefs(arr, q) {
99
- for (const d of arr) {
100
- if ((d.content ?? '').toLowerCase().includes(q)) return true;
101
- }
102
- return false;
103
- }
104
-
105
97
  allLanguages() {
106
98
  const set = new Set();
107
99
  for (const c of this[_items]) {
@@ -3,7 +3,7 @@ import { Concept } from './models/concept.js';
3
3
  import { RelatedConcept } from './models/related-concept.js';
4
4
  import { InvalidInputError, YamlParseError } from './errors.js';
5
5
 
6
- const STRUCTURAL_KEYS = new Set(['termid', 'term']);
6
+ const STRUCTURAL_KEYS = new Set(['termid', 'term', 'figures', 'tables', 'formulas']);
7
7
 
8
8
  export class ConceptParser {
9
9
  parse(raw, context) {
@@ -51,6 +51,9 @@ export class ConceptParser {
51
51
  id: String(doc.termid),
52
52
  term: doc.term || null,
53
53
  localizations,
54
+ figures: doc.figures,
55
+ tables: doc.tables,
56
+ formulas: doc.formulas,
54
57
  raw: doc,
55
58
  });
56
59
  }
@@ -76,6 +79,9 @@ export class ConceptParser {
76
79
  groups: mc.data.groups,
77
80
  dates: mc.dates ?? mc.data?.dates,
78
81
  sources: mc.sources ?? mc.data?.sources,
82
+ figures: mc.data?.figures,
83
+ tables: mc.data?.tables,
84
+ formulas: mc.data?.formulas,
79
85
  status: mc.status,
80
86
  schemaVersion: mc.schema_version,
81
87
  raw: mc,
@@ -12,6 +12,9 @@
12
12
  const DATASET_ASSETS = Object.freeze([
13
13
  { path: 'bibliography.yaml', type: 'file' },
14
14
  { path: 'images', type: 'directory' },
15
+ { path: 'figures', type: 'directory' },
16
+ { path: 'tables', type: 'directory' },
17
+ { path: 'formulas', type: 'directory' },
15
18
  ]);
16
19
 
17
20
  const FILE_ASSETS = Object.freeze(
@@ -0,0 +1,43 @@
1
+ const ENTITY_DIRECTORIES = Object.freeze(new Map([
2
+ ['figure', 'figures'],
3
+ ['table', 'tables'],
4
+ ['formula', 'formulas'],
5
+ ]));
6
+
7
+ const ENTITY_TYPES = Object.freeze([...ENTITY_DIRECTORIES.keys()]);
8
+
9
+ function entityDir(type) {
10
+ const dir = ENTITY_DIRECTORIES.get(type);
11
+ if (!dir) throw new RangeError(`Unknown entity type: ${type}`);
12
+ return dir;
13
+ }
14
+
15
+ function entityPath(type, id) {
16
+ return `${entityDir(type)}/${id}.yaml`;
17
+ }
18
+
19
+ function isKnownEntityType(type) {
20
+ return ENTITY_DIRECTORIES.has(type);
21
+ }
22
+
23
+ function parseEntityPath(zipPath) {
24
+ for (const [type, dir] of ENTITY_DIRECTORIES) {
25
+ const prefix = `${dir}/`;
26
+ if (!zipPath.startsWith(prefix)) continue;
27
+ const filename = zipPath.slice(prefix.length);
28
+ if (!filename.endsWith('.yaml')) continue;
29
+ const id = filename.slice(0, -'.yaml'.length);
30
+ if (!id) continue;
31
+ return { type, id };
32
+ }
33
+ return null;
34
+ }
35
+
36
+ export {
37
+ ENTITY_DIRECTORIES,
38
+ ENTITY_TYPES,
39
+ entityDir,
40
+ entityPath,
41
+ isKnownEntityType,
42
+ parseEntityPath,
43
+ };
package/src/gcr-reader.js CHANGED
@@ -6,6 +6,9 @@ import { COMPILED_FORMATS, parseCompiledPath, compiledPath } from './compiled-fo
6
6
  import { DATASET_ASSETS, findFileAsset, findDirectoryAssetPath } from './dataset-asset.js';
7
7
  import { GcrMetadata } from './models/gcr-metadata.js';
8
8
  import { naturalSort } from './sort.js';
9
+ import { NonVerbalEntity } from './models/non-verbal-entity.js';
10
+ import { BibliographyData } from './models/bibliography-data.js';
11
+ import { entityDir, entityPath, ENTITY_TYPES, parseEntityPath } from './entity-directory.js';
9
12
 
10
13
  export { naturalSort } from './sort.js';
11
14
 
@@ -34,7 +37,7 @@ const BASE64_RE = /^[A-Za-z0-9+/]{100,}={0,2}$/;
34
37
  * @property {Term[]} terms - term designations
35
38
  * @property {Definition[]} [definition] - definition content
36
39
  * @property {{ content: string }[]} [notes] - editorial notes
37
- * @property {{ content: string }[]} [examples] - usage examples
40
+ * @property {Array<{ content: string, examples?: object[], sources?: object[] }>} [examples] - usage examples (may themselves nest examples)
38
41
  * @property {Source[]} [sources] - bibliographic sources
39
42
  * @property {string} [entry_status] - e.g. 'valid', 'draft'
40
43
  * @property {string} [normative_status] - e.g. 'preferred', 'admitted'
@@ -283,12 +286,13 @@ export class GcrPackage {
283
286
  }
284
287
 
285
288
  /**
286
- * Read bibliography.yaml from the package as a string (raw YAML).
287
- * @returns {Promise<string | null>}
289
+ * Read and parse bibliography.yaml from the package as a BibliographyData instance.
290
+ * @returns {Promise<BibliographyData | null>}
288
291
  */
289
292
  async bibliography() {
290
- const fileAsset = DATASET_ASSETS.find((a) => a.type === 'file' && a.path === 'bibliography.yaml');
291
- return fileAsset ? this._readText(fileAsset.path) : null;
293
+ const raw = await this._readText('bibliography.yaml');
294
+ if (raw === null) return null;
295
+ return BibliographyData.fromYAML(raw);
292
296
  }
293
297
 
294
298
  /**
@@ -403,6 +407,50 @@ export class GcrPackage {
403
407
  async _readText(filePath) {
404
408
  return this.readText(filePath);
405
409
  }
410
+
411
+ // --- Non-verbal entity directories (figures/, tables/, formulas/) ---
412
+
413
+ async entityIds(type) {
414
+ const prefix = `${entityDir(type)}/`;
415
+ const ids = [];
416
+ this._zip.forEach((relativePath, entry) => {
417
+ if (!entry.dir && relativePath.startsWith(prefix) && relativePath.endsWith('.yaml')) {
418
+ ids.push(relativePath.slice(prefix.length, -'.yaml'.length));
419
+ }
420
+ });
421
+ return ids.sort(naturalSort);
422
+ }
423
+
424
+ async entity(type, id) {
425
+ const raw = await this.readText(entityPath(type, id));
426
+ if (raw === null) return null;
427
+ const yamlData = yaml.load(raw);
428
+ return NonVerbalEntity.fromData({ ...yamlData, type });
429
+ }
430
+
431
+ async eachEntity(type, callback) {
432
+ for (const id of await this.entityIds(type)) {
433
+ const entity = await this.entity(type, id);
434
+ if (entity) await callback(entity, id);
435
+ }
436
+ }
437
+
438
+ async allEntities(type) {
439
+ const entities = [];
440
+ await this.eachEntity(type, (e) => { entities.push(e); });
441
+ return entities;
442
+ }
443
+
444
+ async entityTypes() {
445
+ const seen = new Set();
446
+ this._zip.forEach((relativePath, entry) => {
447
+ if (!entry.dir) {
448
+ const parsed = parseEntityPath(relativePath);
449
+ if (parsed) seen.add(parsed.type);
450
+ }
451
+ });
452
+ return ENTITY_TYPES.filter((t) => seen.has(t));
453
+ }
406
454
  }
407
455
 
408
456
  // --- Concept YAML parsing ---
package/src/gcr-writer.js CHANGED
@@ -38,7 +38,9 @@ export class GcrWriter {
38
38
  }
39
39
 
40
40
  if (options.bibliography) {
41
- zip.file('bibliography.yaml', options.bibliography);
41
+ const bib = options.bibliography;
42
+ const yamlStr = bib.toYAML ? bib.toYAML() : String(bib);
43
+ zip.file('bibliography.yaml', yamlStr);
42
44
  }
43
45
 
44
46
  if (options.images) {
package/src/index.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  // Models
2
2
  export {
3
3
  GlossaristModel,
4
+ RegistrableModel,
4
5
  Concept, LocalizedConcept,
5
- Designation, Expression, Abbreviation, Symbol, GraphicalSymbol,
6
- Citation, ConceptRef, ConceptSource, RelatedConcept, ConceptDate,
7
- DetailedDefinition, NonVerbRep,
6
+ Designation, Expression, Abbreviation, Symbol, LetterSymbol, GraphicalSymbol,
7
+ Citation, ConceptRef, ConceptSource, RelatedConcept,
8
+ DesignationRelationship, ConceptReference, ConceptDate,
9
+ DetailedDefinition, NonVerbRep, NON_VERBAL_TYPES,
10
+ NonVerbalEntity, SharedNonVerbalEntity,
11
+ Figure, FigureImage, Table, Formula,
12
+ NonVerbalReference, FigureReference, TableReference, FormulaReference,
13
+ BibliographyEntry, BibliographyData,
8
14
  GcrMetadata, GcrStatistics,
9
- RELATIONSHIP_TYPES, DATE_TYPES,
15
+ RELATIONSHIP_TYPES, DESIGNATION_RELATIONSHIP_TYPES, DATE_TYPES,
10
16
  } from './models/index';
11
17
 
12
18
  // GCR reader
@@ -26,16 +32,27 @@ export { ConceptCollection } from './concept-collection';
26
32
  export { ManagedConceptCollection } from './managed-concept-collection';
27
33
 
28
34
  // Validators
29
- export { validateConcept, validateRegister, validateGcrPackage, createConceptValidator, ValidationError, ValidationRule, ValidationResult, RegisterValidator, GcrValidator } from './validators/index';
35
+ export {
36
+ validateConcept, validateRegister, validateGcrPackage,
37
+ createConceptValidator,
38
+ ValidationError, ValidationRule, ValidationResult,
39
+ RegisterValidator, GcrValidator,
40
+ NonVerbalRefIntegrityRule, OrphanedImagesRule,
41
+ } from './validators/index';
42
+ export { AssetIndex } from './validators/asset-index';
30
43
 
31
44
  // UUID
32
45
  export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid';
33
46
 
34
47
  // Reference resolution
35
- export { ReferenceResolver, Reference, referenceResolver } from './reference-resolver';
48
+ export {
49
+ ReferenceResolver, Reference, referenceResolver,
50
+ resolveBibliographyRecord, findNonVerbalEntity,
51
+ } from './reference-resolver';
36
52
 
37
53
  export type MentionParseResult = {
38
- kind: 'cite-ref' | 'urn-ref' | 'numeric' | 'designation' | 'unresolved';
54
+ kind: 'cite-ref' | 'urn-ref' | 'fig-ref' | 'table-ref' | 'formula-ref'
55
+ | 'numeric' | 'designation' | 'unresolved';
39
56
  key?: string;
40
57
  uri?: string;
41
58
  label?: string | null;
@@ -45,6 +62,14 @@ export type MentionParseResult = {
45
62
 
46
63
  export function parseMention(raw: string): MentionParseResult;
47
64
 
65
+ export function fetchLocalizedString(
66
+ hash: Record<string, string> | null,
67
+ lang: string,
68
+ fallback?: string,
69
+ ): string | null;
70
+ export function localizedStringIsEmpty(hash: Record<string, string> | null): boolean;
71
+ export function localizedStringIsPresent(hash: Record<string, string> | null): boolean;
72
+
48
73
  // V1 support
49
74
  export { V1Reader, migrateV1ToV2 } from './v1-reader';
50
75
 
@@ -66,3 +91,11 @@ export const DIRECTORY_ASSETS: readonly { path: string; type: string }[];
66
91
  export function findFileAsset(path: string): { path: string; type: string } | undefined;
67
92
  export function findDirectoryAssetPath(zipPath: string): { path: string; type: string } | undefined;
68
93
  export function isDatasetAssetPath(zipPath: string): boolean;
94
+
95
+ // Entity directory registry
96
+ export const ENTITY_DIRECTORIES: ReadonlyMap<string, string>;
97
+ export const ENTITY_TYPES: readonly string[];
98
+ export function entityDir(type: string): string;
99
+ export function entityPath(type: string, id: string): string;
100
+ export function isKnownEntityType(type: string): boolean;
101
+ export function parseEntityPath(zipPath: string): { type: string; id: string } | null;
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ export { ConceptCollection } from './concept-collection.js';
7
7
  export { ManagedConceptCollection } from './managed-concept-collection.js';
8
8
  export { validateConcept, validateRegister, validateGcrPackage, createConceptValidator, ValidationError, ValidationRule, ValidationResult, RegisterValidator, GcrValidator } from './validators/index.js';
9
9
  export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid.js';
10
- export { ReferenceResolver, Reference, referenceResolver, resolveBibliographyRecord } from './reference-resolver.js';
10
+ export { ReferenceResolver, Reference, referenceResolver, resolveBibliographyRecord, findNonVerbalEntity } from './reference-resolver.js';
11
11
  export { parseMention } from './reference-mention.js';
12
12
  export { ReferenceClassifier } from './render-classification.js';
13
13
  export { V1Reader, migrateV1ToV2 } from './v1-reader.js';
@@ -30,12 +30,29 @@ export {
30
30
 
31
31
  export {
32
32
  GlossaristModel,
33
+ RegistrableModel,
33
34
  Register, Section,
34
35
  REGISTER_STATUSES, ORDERING_METHODS,
35
36
  Concept, LocalizedConcept,
36
37
  Designation, Expression, Abbreviation, Symbol, GraphicalSymbol,
37
38
  Citation, ConceptRef, ConceptSource, RelatedConcept, DesignationRelationship, ConceptReference, ConceptDate,
38
- DetailedDefinition, NonVerbRep,
39
+ DetailedDefinition, NonVerbRep, NON_VERBAL_TYPES,
40
+ NonVerbalEntity, SharedNonVerbalEntity,
41
+ Figure, FigureImage, Table, Formula,
42
+ NonVerbalReference, FigureReference, TableReference, FormulaReference,
43
+ BibliographyEntry, BibliographyData,
44
+ fetchLocalizedString, localizedStringIsEmpty, localizedStringIsPresent,
39
45
  GcrMetadata, GcrStatistics,
40
46
  RELATIONSHIP_TYPES, DESIGNATION_RELATIONSHIP_TYPES, DATE_TYPES,
41
47
  } from './models/index.js';
48
+
49
+ export { AssetIndex } from './validators/asset-index.js';
50
+
51
+ export {
52
+ ENTITY_DIRECTORIES,
53
+ ENTITY_TYPES,
54
+ entityDir,
55
+ entityPath,
56
+ isKnownEntityType,
57
+ parseEntityPath,
58
+ } from './entity-directory.js';
@@ -3,6 +3,7 @@ import { readConcepts, readRegister } from './concept-reader.js';
3
3
  import { writeConcepts } from './concept-writer.js';
4
4
  import { loadGcr } from './gcr-reader.js';
5
5
  import { GcrWriter } from './gcr-writer.js';
6
+ import { BibliographyData } from './models/bibliography-data.js';
6
7
 
7
8
  export class ManagedConceptCollection {
8
9
  constructor() {
@@ -73,8 +74,16 @@ export class ManagedConceptCollection {
73
74
  return this;
74
75
  }
75
76
 
76
- setBibliography(yamlString) {
77
- this._bibliography = yamlString;
77
+ setBibliography(bib) {
78
+ if (bib instanceof BibliographyData) {
79
+ this._bibliography = bib;
80
+ } else if (typeof bib === 'string') {
81
+ this._bibliography = BibliographyData.fromYAML(bib);
82
+ } else if (bib == null) {
83
+ this._bibliography = null;
84
+ } else {
85
+ this._bibliography = new BibliographyData(bib);
86
+ }
78
87
  return this;
79
88
  }
80
89
 
@@ -0,0 +1,43 @@
1
+ import yaml from 'js-yaml';
2
+ import { GlossaristModel } from './base.js';
3
+ import { BibliographyEntry } from './bibliography-entry.js';
4
+
5
+ export class BibliographyData extends GlossaristModel {
6
+ constructor(data = {}) {
7
+ super();
8
+ const entriesData = data.bibliography ?? data.entries ?? [];
9
+ this._rawEntries = Array.isArray(entriesData) ? entriesData : [];
10
+ this._entries = null;
11
+ }
12
+
13
+ get entries() {
14
+ return this._lazy('_entries', '_rawEntries',
15
+ e => e instanceof BibliographyEntry ? e : new BibliographyEntry(e));
16
+ }
17
+
18
+ find(id) {
19
+ return this.entries.find(e => e.id === id) ?? null;
20
+ }
21
+
22
+ get keys() {
23
+ return this.entries.map(e => e.id);
24
+ }
25
+
26
+ toJSON() {
27
+ if (this.entries.length === 0) return { bibliography: [] };
28
+ return { bibliography: this.entries.map(e => e.toJSON()) };
29
+ }
30
+
31
+ toYAML() {
32
+ return yaml.dump(this.toJSON());
33
+ }
34
+
35
+ static fromYAML(yamlString) {
36
+ const parsed = yaml.load(yamlString);
37
+ return new BibliographyData(parsed ?? {});
38
+ }
39
+
40
+ static fromJSON(data) {
41
+ return new BibliographyData(data);
42
+ }
43
+ }
@@ -0,0 +1,26 @@
1
+ import { GlossaristModel } from './base.js';
2
+
3
+ export class BibliographyEntry extends GlossaristModel {
4
+ constructor(data = {}) {
5
+ super();
6
+ this.id = data.id ?? null;
7
+ this.reference = data.reference ?? null;
8
+ this.title = data.title ?? null;
9
+ this.link = data.link ?? null;
10
+ this.type = data.type ?? null;
11
+ }
12
+
13
+ toJSON() {
14
+ const obj = {};
15
+ if (this.id != null) obj.id = this.id;
16
+ if (this.reference != null) obj.reference = this.reference;
17
+ if (this.title != null) obj.title = this.title;
18
+ if (this.link != null) obj.link = this.link;
19
+ if (this.type != null) obj.type = this.type;
20
+ return obj;
21
+ }
22
+
23
+ static fromJSON(data) {
24
+ return new BibliographyEntry(data);
25
+ }
26
+ }
@@ -4,6 +4,9 @@ import { RelatedConcept } from './related-concept.js';
4
4
  import { ConceptReference } from './concept-reference.js';
5
5
  import { ConceptDate } from './concept-date.js';
6
6
  import { ConceptSource } from './concept-source.js';
7
+ import { FigureReference } from './non-verbal-references.js';
8
+ import { TableReference } from './non-verbal-references.js';
9
+ import { FormulaReference } from './non-verbal-references.js';
7
10
 
8
11
  export class Concept extends GlossaristModel {
9
12
  constructor(data = {}) {
@@ -19,6 +22,12 @@ export class Concept extends GlossaristModel {
19
22
  this.tags = Array.isArray(data.tags) ? [...data.tags] : [];
20
23
  this.dates = _mapInstances(data.dates ?? [], ConceptDate);
21
24
  this.sources = _mapInstances(data.sources ?? [], ConceptSource);
25
+ this._rawFigures = data.figures ?? [];
26
+ this._rawTables = data.tables ?? [];
27
+ this._rawFormulas = data.formulas ?? [];
28
+ this._figures = null;
29
+ this._tables = null;
30
+ this._formulas = null;
22
31
  this.status = data.status ?? null;
23
32
  this.schemaVersion = data.schemaVersion ?? data.schema_version ?? '3';
24
33
  this.raw = data.raw ?? null;
@@ -107,6 +116,38 @@ export class Concept extends GlossaristModel {
107
116
  return null;
108
117
  }
109
118
 
119
+ get figures() {
120
+ return this._lazy('_figures', '_rawFigures',
121
+ r => FigureReference.fromJSON(r));
122
+ }
123
+
124
+ get tables() {
125
+ return this._lazy('_tables', '_rawTables',
126
+ r => TableReference.fromJSON(r));
127
+ }
128
+
129
+ get formulas() {
130
+ return this._lazy('_formulas', '_rawFormulas',
131
+ r => FormulaReference.fromJSON(r));
132
+ }
133
+
134
+ /**
135
+ * Yield every content-text fragment in this concept across all
136
+ * localizations, recursing through nested examples. Each yielded
137
+ * `{ text, source }` carries a dotted path rooted at
138
+ * `localizations.<lang>.<slot>[i]...content`.
139
+ *
140
+ * Designations are not included; they live on `LocalizedConcept.terms`
141
+ * and have a different shape.
142
+ */
143
+ *walkTexts() {
144
+ for (const lang of this.languages) {
145
+ const lc = this.localization(lang);
146
+ if (!lc) continue;
147
+ yield* lc.walkTexts(`localizations.${lang}`);
148
+ }
149
+ }
150
+
110
151
  toJSON() {
111
152
  const obj = { id: this.id };
112
153
  if (this.term != null) obj.term = this.term;
@@ -141,6 +182,9 @@ export class Concept extends GlossaristModel {
141
182
  if (this.sources.length > 0) {
142
183
  obj.sources = this.sources.map(s => s.toJSON());
143
184
  }
185
+ this._serialize(obj, 'figures', '_figures', '_rawFigures');
186
+ this._serialize(obj, 'tables', '_tables', '_rawTables');
187
+ this._serialize(obj, 'formulas', '_formulas', '_rawFormulas');
144
188
  if (this.status != null) obj.status = this.status;
145
189
  obj.schema_version = this.schemaVersion;
146
190
  return obj;
@@ -1,23 +1,11 @@
1
- import { GlossaristModel } from './base.js';
1
+ import { RegistrableModel } from './registrable.js';
2
2
  import { ConceptSource } from './concept-source.js';
3
3
  import { Pronunciation } from './pronunciation.js';
4
4
  import { GrammarInfo } from './grammar-info.js';
5
5
  import { RelatedConcept } from './related-concept.js';
6
6
  import { DesignationRelationship, DESIGNATION_RELATIONSHIP_TYPES } from './designation-relationship.js';
7
7
 
8
- export class Designation extends GlossaristModel {
9
- static _registry = new Map();
10
-
11
- static register(type, cls) {
12
- Designation._registry.set(type, cls);
13
- }
14
-
15
- static fromData(data) {
16
- if (data instanceof Designation) return data;
17
- const Cls = Designation._registry.get(data?.type) ?? Designation;
18
- return new Cls(data);
19
- }
20
-
8
+ export class Designation extends RegistrableModel {
21
9
  constructor(data = {}) {
22
10
  super();
23
11
  this.designation = data.designation ?? '';
@@ -5,19 +5,43 @@ export class DetailedDefinition extends GlossaristModel {
5
5
  constructor(data = {}) {
6
6
  super();
7
7
  this.content = data.content ?? '';
8
- this.sources = (data.sources ?? []).map(
9
- s => s instanceof Citation ? s : new Citation(s)
10
- );
8
+ this._rawSources = data.sources ?? [];
9
+ this._rawExamples = data.examples ?? [];
10
+ this._sources = null;
11
+ this._examples = null;
12
+ }
13
+
14
+ get sources() {
15
+ return this._lazy('_sources', '_rawSources',
16
+ s => s instanceof Citation ? s : new Citation(s));
17
+ }
18
+
19
+ get examples() {
20
+ return this._lazy('_examples', '_rawExamples',
21
+ e => e instanceof DetailedDefinition ? e : new DetailedDefinition(e));
11
22
  }
12
23
 
13
24
  toJSON() {
14
25
  const obj = { content: this.content };
15
- if (this.sources.length > 0) {
16
- obj.sources = this.sources.map(s => s.toJSON());
17
- }
26
+ this._serialize(obj, 'sources', '_sources', '_rawSources');
27
+ this._serialize(obj, 'examples', '_examples', '_rawExamples');
18
28
  return obj;
19
29
  }
20
30
 
31
+ /**
32
+ * Yield this definition's content and the content of every nested
33
+ * example (recursively). Each item carries `{ text, source }` where
34
+ * `source` is `<path>.content` rooted at the `path` argument.
35
+ */
36
+ *walkTexts(path) {
37
+ if (typeof this.content === 'string' && this.content.length > 0) {
38
+ yield { text: this.content, source: `${path}.content` };
39
+ }
40
+ for (let i = 0; i < this.examples.length; i++) {
41
+ yield* this.examples[i].walkTexts(`${path}.examples[${i}]`);
42
+ }
43
+ }
44
+
21
45
  static fromJSON(data) {
22
46
  return new DetailedDefinition(data);
23
47
  }
@@ -0,0 +1,71 @@
1
+ import { SharedNonVerbalEntity } from './shared-non-verbal-entity.js';
2
+ import { NonVerbalEntity } from './non-verbal-entity.js';
3
+
4
+ export class FigureImage {
5
+ constructor(data = {}) {
6
+ this.src = data.src ?? null;
7
+ this.format = data.format ?? null;
8
+ this.role = data.role ?? null;
9
+ this.width = data.width ?? null;
10
+ this.height = data.height ?? null;
11
+ this.scale = data.scale ?? null;
12
+ }
13
+
14
+ toJSON() {
15
+ const obj = {};
16
+ if (this.src != null) obj.src = this.src;
17
+ if (this.format != null) obj.format = this.format;
18
+ if (this.role != null) obj.role = this.role;
19
+ if (this.width != null) obj.width = this.width;
20
+ if (this.height != null) obj.height = this.height;
21
+ if (this.scale != null) obj.scale = this.scale;
22
+ return obj;
23
+ }
24
+
25
+ static fromJSON(data) { return new FigureImage(data); }
26
+ }
27
+
28
+ export class Figure extends SharedNonVerbalEntity {
29
+ constructor(data = {}) {
30
+ super(data);
31
+ this._rawImages = data.images ?? [];
32
+ this._rawSubfigures = data.subfigures ?? [];
33
+ this._images = null;
34
+ this._subfigures = null;
35
+ }
36
+
37
+ get images() {
38
+ return this._lazy('_images', '_rawImages',
39
+ i => i instanceof FigureImage ? i : new FigureImage(i));
40
+ }
41
+
42
+ get subfigures() {
43
+ return this._lazy('_subfigures', '_rawSubfigures',
44
+ s => s instanceof Figure ? s : new Figure(s));
45
+ }
46
+
47
+ findById(targetId) {
48
+ if (this.id === targetId) return this;
49
+ for (const sub of this.subfigures) {
50
+ const found = sub.findById(targetId);
51
+ if (found) return found;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ allIds() {
57
+ const ids = this.id != null ? [this.id] : [];
58
+ return [...ids, ...this.subfigures.flatMap(s => s.allIds())];
59
+ }
60
+
61
+ toJSON() {
62
+ const obj = super.toJSON();
63
+ this._serialize(obj, 'images', '_images', '_rawImages');
64
+ this._serialize(obj, 'subfigures', '_subfigures', '_rawSubfigures');
65
+ return obj;
66
+ }
67
+
68
+ static fromJSON(data) { return new Figure(data); }
69
+ }
70
+
71
+ NonVerbalEntity.register('figure', Figure);
@@ -0,0 +1,21 @@
1
+ import { SharedNonVerbalEntity } from './shared-non-verbal-entity.js';
2
+ import { NonVerbalEntity } from './non-verbal-entity.js';
3
+
4
+ export class Formula extends SharedNonVerbalEntity {
5
+ constructor(data = {}) {
6
+ super(data);
7
+ this.expression = data.expression ?? null;
8
+ this.notation = data.notation ?? null;
9
+ }
10
+
11
+ toJSON() {
12
+ const obj = super.toJSON();
13
+ if (this.expression != null) obj.expression = this.expression;
14
+ if (this.notation != null) obj.notation = this.notation;
15
+ return obj;
16
+ }
17
+
18
+ static fromJSON(data) { return new Formula(data); }
19
+ }
20
+
21
+ NonVerbalEntity.register('formula', Formula);