glossarist 0.3.7 → 0.4.0

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.
@@ -2,35 +2,32 @@ import { ConceptRef } from './models/concept-ref.js';
2
2
  import { parseMention } from './reference-mention.js';
3
3
 
4
4
  export class Reference {
5
- /**
6
- * @param {string} type — the structural kind of the reference
7
- * ('concept', 'dataset', 'bibliography', 'typed-ref',
8
- * 'standard').
9
- * @param {string | null} target — the legacy flat display
10
- * string. Kept for backward compat with callers that only
11
- * read `r.target`.
12
- * @param {string | null} [relationship] — the type of the
13
- * relationship that produced this reference (e.g. 'see',
14
- * 'supersedes', 'source').
15
- * @param {string | null} [source] — a JSON-pointer-ish path
16
- * indicating where in the concept the reference was
17
- * extracted from.
18
- * @param {object} [extras] — additional fields (v8+):
19
- * `citation`, `sourceId`, `resolution`, `lookupKey`,
20
- * `label`, `quoted`, `uri`. All optional.
21
- */
22
5
  constructor(type, target, relationship, source, extras = {}) {
23
6
  this.type = type;
24
7
  this.target = target;
25
8
  this.relationship = relationship ?? null;
26
9
  this.source = source ?? null;
27
-
28
10
  this.uri = extras.uri ?? null;
29
11
  this.citation = extras.citation ?? null;
30
12
  this.sourceId = extras.sourceId ?? null;
31
13
  this.resolution = extras.resolution ?? null;
32
14
  this.lookupKey = extras.lookupKey ?? null;
33
15
  }
16
+
17
+ get dedupKey() {
18
+ if (this.type === 'bibliography') {
19
+ return ['bibliography',
20
+ this.sourceId ?? this.citation?.ref?.id ?? this.target];
21
+ }
22
+ if (this.type === 'figure' || this.type === 'table' || this.type === 'formula') {
23
+ return [this.type, this.lookupKey?.entityId ?? this.target];
24
+ }
25
+ if (this.type === 'concept') {
26
+ return ['concept',
27
+ this.lookupKey?.id ?? this.lookupKey?.designation ?? this.target];
28
+ }
29
+ return [this.type, this.target];
30
+ }
34
31
  }
35
32
 
36
33
  function refTarget(rc) {
@@ -41,43 +38,66 @@ function refTarget(rc) {
41
38
  return '';
42
39
  }
43
40
 
41
+ export function resolveBibliographyRecord(citationRef, registry) {
42
+ if (!citationRef?.source || !citationRef?.id) return null;
43
+ const bioColl = registry[`bibliography:${citationRef.source}`]?.concepts;
44
+ if (!bioColl) return null;
45
+ if (citationRef.version) {
46
+ return bioColl.byIdAnd(citationRef.id, citationRef.version);
47
+ }
48
+ return bioColl.byId(citationRef.id);
49
+ }
50
+
51
+ export function findNonVerbalEntity(ref, registry) {
52
+ const { entityType, entityId } = ref.lookupKey ?? {};
53
+ if (!entityType || !entityId) return null;
54
+ const collection = registry[`nvr:${entityType}`];
55
+ if (!collection) return null;
56
+ for (const entity of collection) {
57
+ const found = entity.findById(entityId);
58
+ if (found) return found;
59
+ }
60
+ return null;
61
+ }
62
+
44
63
  export class ReferenceResolver {
45
- /**
46
- * Extract all embedded references from a concept's localizations.
47
- *
48
- * Walks definitions, notes, examples, and annotations text.
49
- * For each `{{...}}` mention, runs `parseMention` to
50
- * classify the form, then dispatches:
51
- * - 'cite-ref' → look up the key in concept.sources; emit
52
- * Bibliography Reference with the Citation.
53
- * - 'numeric' → emit Concept Reference with the bare id
54
- * (existing behavior).
55
- * - 'unresolved' → do not emit a Reference.
56
- *
57
- * @param {Concept} concept
58
- * @returns {Reference[]}
59
- */
60
64
  extractReferences(concept) {
61
65
  const refs = [];
62
66
 
63
67
  for (const rc of concept.relatedConcepts) {
64
68
  const target = refTarget(rc);
65
69
  if (target) {
66
- refs.push(new Reference('concept', target, rc.type, 'relatedConcepts'));
70
+ refs.push(new Reference('concept', target, rc.type, 'relatedConcepts', {
71
+ lookupKey: { id: target },
72
+ }));
67
73
  }
68
74
  }
69
75
 
76
+ for (const ref of concept.figures) {
77
+ refs.push(new Reference('figure', ref.display ?? ref.entityId, 'structural', 'figures', {
78
+ lookupKey: { entityType: 'figure', entityId: ref.entityId },
79
+ }));
80
+ }
81
+ for (const ref of concept.tables) {
82
+ refs.push(new Reference('table', ref.display ?? ref.entityId, 'structural', 'tables', {
83
+ lookupKey: { entityType: 'table', entityId: ref.entityId },
84
+ }));
85
+ }
86
+ for (const ref of concept.formulas) {
87
+ refs.push(new Reference('formula', ref.display ?? ref.entityId, 'structural', 'formulas', {
88
+ lookupKey: { entityType: 'formula', entityId: ref.entityId },
89
+ }));
90
+ }
91
+
70
92
  for (const lang of concept.languages) {
71
93
  const lc = concept.localization(lang);
72
94
  if (!lc) continue;
73
95
 
74
- if (lc.sources) {
75
- for (let i = 0; i < lc.sources.length; i++) {
76
- const src = lc.sources[i];
77
- const ref = src.origin?.toString() ?? '';
78
- if (ref) {
79
- refs.push(new Reference('standard', ref, src.type, `localizations.${lang}.sources[${i}]`));
80
- }
96
+ for (let i = 0; i < lc.sources.length; i++) {
97
+ const src = lc.sources[i];
98
+ const ref = src.origin?.toString() ?? '';
99
+ if (ref) {
100
+ refs.push(new Reference('standard', ref, src.type, `localizations.${lang}.sources[${i}]`));
81
101
  }
82
102
  }
83
103
 
@@ -89,40 +109,40 @@ export class ReferenceResolver {
89
109
  }
90
110
  }
91
111
 
92
- return refs;
112
+ return this._dedup(refs);
113
+ }
114
+
115
+ _dedup(refs) {
116
+ const seen = new Set();
117
+ return refs.filter(ref => {
118
+ const key = JSON.stringify(ref.dedupKey);
119
+ if (seen.has(key)) return false;
120
+ seen.add(key);
121
+ return true;
122
+ });
93
123
  }
94
124
 
95
- /**
96
- * Collect all text fields from a localized concept, paired
97
- * with diagnostic source paths.
98
- *
99
- * @param {LocalizedConcept} lc
100
- * @param {string} lang
101
- * @returns {{text: string, source: string}[]}
102
- */
103
125
  _collectTexts(lc, lang) {
104
126
  const out = [];
105
- for (let i = 0; (lc.definitions ?? [])[i]; i++) {
127
+ for (let i = 0; i < lc.definitions.length; i++) {
106
128
  const content = lc.definitions[i]?.content;
107
129
  if (typeof content === 'string') {
108
130
  out.push({ text: content, source: `localizations.${lang}.definitions[${i}].content` });
109
131
  }
110
132
  }
111
- for (let i = 0; (lc.notes ?? [])[i]; i++) {
112
- const content = typeof lc.notes[i] === 'object'
113
- ? (lc.notes[i]?.content ?? '')
114
- : String(lc.notes[i] ?? '');
133
+ for (let i = 0; i < lc.notes.length; i++) {
134
+ const content = lc.notes[i]?.content;
115
135
  if (content) {
116
136
  out.push({ text: content, source: `localizations.${lang}.notes[${i}].content` });
117
137
  }
118
138
  }
119
- for (let i = 0; (lc.examples ?? [])[i]; i++) {
139
+ for (let i = 0; i < lc.examples.length; i++) {
120
140
  const content = lc.examples[i]?.content;
121
141
  if (typeof content === 'string') {
122
142
  out.push({ text: content, source: `localizations.${lang}.examples[${i}].content` });
123
143
  }
124
144
  }
125
- for (let i = 0; (lc.annotations ?? [])[i]; i++) {
145
+ for (let i = 0; i < lc.annotations.length; i++) {
126
146
  const content = lc.annotations[i]?.content;
127
147
  if (typeof content === 'string') {
128
148
  out.push({ text: content, source: `localizations.${lang}.annotations[${i}].content` });
@@ -131,15 +151,6 @@ export class ReferenceResolver {
131
151
  return out;
132
152
  }
133
153
 
134
- /**
135
- * Walk a single text string and emit References for each
136
- * `{{...}}` mention.
137
- *
138
- * @param {string} text
139
- * @param {string} source — diagnostic path
140
- * @param {Concept} concept — the owning concept (for cite-ref lookup)
141
- * @returns {Reference[]}
142
- */
143
154
  _extractFromText(text, source, concept) {
144
155
  const refs = [];
145
156
  const re = /\{\{([^{}]*?)\}\}/g;
@@ -156,6 +167,21 @@ export class ReferenceResolver {
156
167
  resolution: null,
157
168
  }));
158
169
  break;
170
+ case 'fig-ref':
171
+ refs.push(new Reference('figure', parsed.label ?? parsed.key, 'embedded', source, {
172
+ lookupKey: { entityType: 'figure', entityId: parsed.key },
173
+ }));
174
+ break;
175
+ case 'table-ref':
176
+ refs.push(new Reference('table', parsed.label ?? parsed.key, 'embedded', source, {
177
+ lookupKey: { entityType: 'table', entityId: parsed.key },
178
+ }));
179
+ break;
180
+ case 'formula-ref':
181
+ refs.push(new Reference('formula', parsed.label ?? parsed.key, 'embedded', source, {
182
+ lookupKey: { entityType: 'formula', entityId: parsed.key },
183
+ }));
184
+ break;
159
185
  case 'numeric':
160
186
  refs.push(new Reference('concept', parsed.label ?? parsed.id, 'embedded', source, {
161
187
  lookupKey: { id: parsed.id },
@@ -173,17 +199,6 @@ export class ReferenceResolver {
173
199
  return refs;
174
200
  }
175
201
 
176
- /**
177
- * Resolve a `cite-ref` parser result against the concept's
178
- * sources list. Emits a Bibliography Reference with the
179
- * resolved Citation (if found) or an unresolved Reference
180
- * (if not).
181
- *
182
- * @param {MentionParseResult} parsed
183
- * @param {string} source — diagnostic path
184
- * @param {Concept} concept — the owning concept
185
- * @returns {Reference}
186
- */
187
202
  _resolveCiteRef(parsed, source, concept) {
188
203
  const sourceEntry = concept?.findSourceById(parsed.key) ?? null;
189
204
  if (!sourceEntry) {
@@ -215,124 +230,71 @@ export class ReferenceResolver {
215
230
  );
216
231
  }
217
232
 
218
- /**
219
- * Resolve a single reference against a registry (a map of
220
- * datasetId → { concepts, register? }). The registry may also
221
- * include 'bibliography:<source>' keys for bibliographic
222
- * datasets.
223
- *
224
- * For a `type: 'bibliography'` Reference with an inline
225
- * `citation`, the resolver first tries the bibliography
226
- * registry (matching `citation.ref` by source/id/version);
227
- * if not found, returns the inline Citation as a
228
- * self-contained fallback.
229
- *
230
- * For a `type: 'bibliography'` Reference with a `uri` and
231
- * `resolution.kind === 'bibliography-namespace'`, the
232
- * resolver tries the bibliography registry by
233
- * `resolution.source/id/version`.
234
- *
235
- * For `type: 'concept'` References with a `lookupKey.id`
236
- * (id-match, short-id, or numeric), the resolver looks up
237
- * the id in `lookupKey.dataset`'s ConceptCollection.
238
- *
239
- * Backward compat: when the second argument is a
240
- * ConceptCollection (has `byId` but no `concepts` field), it
241
- * is treated as a one-key registry of one default dataset.
242
- */
243
233
  resolveReference(ref, registry) {
244
- if (ref == null) return null;
234
+ if (ref == null || registry == null) return null;
245
235
 
246
- // Backward-compat: single ConceptCollection becomes a
247
- // one-key registry.
248
- if (isConceptCollection(registry)) {
249
- registry = { _default: { concepts: registry } };
236
+ switch (ref.type) {
237
+ case 'concept': return this._resolveConcept(ref, registry);
238
+ case 'bibliography': return this._resolveBibliography(ref, registry);
239
+ case 'figure':
240
+ case 'table':
241
+ case 'formula': return this._resolveNonVerbal(ref, registry);
242
+ case 'dataset': return this._resolveDataset(ref, registry);
243
+ case 'typed-ref': return this._resolveTypedRef(ref, registry);
244
+ case 'standard': return this._resolveStandard(ref, registry);
245
+ default: return null;
250
246
  }
251
- if (registry == null) return null;
247
+ }
252
248
 
253
- // 1. cite:key form (Bibliography with inline Citation).
254
- if (ref.type === 'bibliography' && ref.citation) {
255
- const bioRecord = this._resolveBibliographyRecord(
256
- ref.citation.ref,
257
- registry,
258
- );
259
- if (bioRecord) return bioRecord;
260
- return ref.citation;
261
- }
249
+ _resolveNonVerbal(ref, registry) {
250
+ return findNonVerbalEntity(ref, registry);
251
+ }
262
252
 
263
- // 2. URI form (urn:... or https:...) with
264
- // bibliography-namespace resolution.
265
- if (ref.uri) {
266
- if (ref.resolution?.kind === 'bibliography-namespace'
267
- || (ref.resolution?.source && !ref.resolution?.datasetId)) {
268
- const bioRecord = this._resolveBibliographyRecord(
269
- ref.resolution,
270
- registry,
271
- );
272
- if (bioRecord) return bioRecord;
253
+ _resolveConcept(ref, registry) {
254
+ if (ref.lookupKey?.id) {
255
+ const dataset = ref.lookupKey.dataset;
256
+ if (dataset) {
257
+ return registry[dataset]?.concepts?.byId(ref.lookupKey.id) ?? null;
273
258
  }
274
- // Concept URI lookup (for non-bibliography URIs).
275
- if (ref.resolution?.datasetId) {
276
- const coll = registry[ref.resolution.datasetId]?.concepts;
277
- if (coll) {
278
- return coll.byId(ref.resolution.conceptId);
279
- }
259
+ for (const entry of Object.values(registry)) {
260
+ const found = entry?.concepts?.byId(ref.lookupKey.id);
261
+ if (found) return found;
280
262
  }
281
263
  return null;
282
264
  }
283
-
284
- // 3. Same-dataset concept id (numeric mention, id-match, etc.).
285
- if (ref.lookupKey?.id) {
286
- const coll = registry[ref.lookupKey.dataset]?.concepts;
287
- if (coll) return coll.byId(ref.lookupKey.id);
288
- return null;
265
+ if (ref.uri && ref.resolution?.datasetId) {
266
+ return registry[ref.resolution.datasetId]?.concepts?.byId(ref.resolution.conceptId) ?? null;
289
267
  }
268
+ return null;
269
+ }
290
270
 
291
- // 3b. Backward-compat: a concept ref with a `target` (id)
292
- // but no `lookupKey` is looked up in the single
293
- // collection (backward-compat one-key registry).
294
- if (ref.type === 'concept' && ref.target) {
295
- const defaultColl = registry._default?.concepts;
296
- if (defaultColl) return defaultColl.byId(ref.target);
297
- // Try every dataset in the registry as a fallback.
298
- for (const entry of Object.values(registry)) {
299
- if (entry?.concepts?.byId(ref.target)) {
300
- return entry.concepts.byId(ref.target);
301
- }
302
- }
303
- return null;
271
+ _resolveBibliography(ref, registry) {
272
+ if (ref.citation) {
273
+ return resolveBibliographyRecord(ref.citation.ref, registry) ?? ref.citation;
304
274
  }
305
-
306
- // 4. Unanchored designation — search is a separate concern
307
- // (plan 06). For v8, return null.
308
- if (ref.lookupKey?.designation) {
309
- return null;
275
+ if (ref.uri && ref.resolution?.source) {
276
+ return resolveBibliographyRecord(ref.resolution, registry) ?? null;
310
277
  }
278
+ return null;
279
+ }
311
280
 
281
+ _resolveDataset(_ref, _registry) {
312
282
  return null;
313
283
  }
314
284
 
315
- /**
316
- * Try to resolve a Citation::Ref against the bibliography
317
- * registry. The ref has shape { source, id, version? }.
318
- *
319
- * Returns the matching bibliographic record (a Concept), or
320
- * null if no match.
321
- */
322
- _resolveBibliographyRecord(citationRef, registry) {
323
- if (!citationRef?.source || !citationRef?.id) return null;
324
- const bioColl = registry[`bibliography:${citationRef.source}`]?.concepts;
325
- if (!bioColl) return null;
326
- if (citationRef.version) {
327
- return bioColl.byIdAnd(citationRef.id, citationRef.version);
328
- }
329
- return bioColl.byId(citationRef.id);
285
+ _resolveTypedRef(_ref, _registry) {
286
+ return null;
287
+ }
288
+
289
+ _resolveStandard(_ref, _registry) {
290
+ return null;
330
291
  }
331
292
 
332
293
  resolveAll(concept, registry) {
333
294
  const resolved = new Map();
334
295
  for (const ref of this.extractReferences(concept)) {
335
- if (ref.type === 'concept' || ref.type === 'bibliography') {
296
+ if (ref.type === 'concept' || ref.type === 'bibliography'
297
+ || ref.type === 'figure' || ref.type === 'table' || ref.type === 'formula') {
336
298
  const target = this.resolveReference(ref, registry);
337
299
  if (target != null) {
338
300
  const key = ref.target ?? ref.uri ?? ref.sourceId;
@@ -344,15 +306,4 @@ export class ReferenceResolver {
344
306
  }
345
307
  }
346
308
 
347
- /**
348
- * Type-guard for the single-collection case (backward compat).
349
- * A ConceptCollection has `byId` but no `concepts` field.
350
- */
351
- function isConceptCollection(x) {
352
- return x != null
353
- && typeof x === 'object'
354
- && typeof x.byId === 'function'
355
- && !('concepts' in x);
356
- }
357
-
358
309
  export const referenceResolver = new ReferenceResolver();
@@ -1,47 +1,29 @@
1
- /**
2
- * Classify a Reference for rendering.
3
- *
4
- * The classifier is constructed once per render with a registry
5
- * (and optional source dataset id). The classify() method is
6
- * pure and side-effect-free.
7
- *
8
- * Each `Reference.type` is its own `_classifyXxx` method. The
9
- * dispatch in classify() is closed for modification.
10
- */
1
+ import { resolveBibliographyRecord, findNonVerbalEntity } from './reference-resolver.js';
11
2
 
12
3
  export class ReferenceClassifier {
13
- /**
14
- * @param {object} registry — the deployment's dataset registry.
15
- * @param {string} [sourceDatasetId] — the dataset the source
16
- * concept belongs to; used to determine "same-dataset".
17
- * @param {object} [options] — additional options (e.g. scope).
18
- */
19
4
  constructor(registry = {}, sourceDatasetId = null, options = {}) {
20
5
  this.registry = registry;
21
6
  this.sourceDatasetId = sourceDatasetId;
22
7
  this.options = options;
23
8
  }
24
9
 
25
- /**
26
- * @param {Reference} ref
27
- * @returns {string} — the classification (e.g. 'same-dataset',
28
- * 'internal-citation', 'unresolved', etc.)
29
- */
30
10
  classify(ref) {
31
11
  if (ref == null) return 'unknown';
32
12
 
33
13
  switch (ref.type) {
34
- case 'concept': return this._classifyConcept(ref);
35
- case 'dataset': return this._classifyDataset(ref);
14
+ case 'concept': return this._classifyConcept(ref);
15
+ case 'dataset': return this._classifyDataset(ref);
36
16
  case 'bibliography': return this._classifyBibliography(ref);
37
- case 'typed-ref': return this._classifyTypedRef(ref);
38
- case 'standard': return 'legacy-standard';
39
- default: return 'unknown';
17
+ case 'figure':
18
+ case 'table':
19
+ case 'formula': return this._classifyNonVerbal(ref);
20
+ case 'typed-ref': return this._classifyTypedRef(ref);
21
+ case 'standard': return 'legacy-standard';
22
+ default: return 'unknown';
40
23
  }
41
24
  }
42
25
 
43
26
  _classifyConcept(ref) {
44
- // 1. URI form, resolved to a dataset.
45
27
  if (ref.uri) {
46
28
  const dsId = ref.resolution?.datasetId;
47
29
  if (!dsId) return 'unresolved';
@@ -49,21 +31,15 @@ export class ReferenceClassifier {
49
31
  if (dsId === this.sourceDatasetId) return 'same-dataset';
50
32
  return 'cross-dataset';
51
33
  }
52
- // 2. Unanchored designation.
53
34
  if (ref.lookupKey?.designation) {
54
35
  return 'unresolved-designation';
55
36
  }
56
- // 3. Id-style (id-match, short-id, numeric).
57
37
  if (ref.lookupKey?.id) {
58
38
  const dsId = ref.lookupKey.dataset;
59
39
  if (!this.registry[dsId]) return 'unresolved';
60
40
  if (dsId === this.sourceDatasetId) return 'same-dataset';
61
41
  return 'cross-dataset';
62
42
  }
63
- // 4. Concept ref with target (legacy).
64
- if (ref.target) {
65
- return 'unresolved';
66
- }
67
43
  return 'unresolved';
68
44
  }
69
45
 
@@ -74,18 +50,15 @@ export class ReferenceClassifier {
74
50
  }
75
51
 
76
52
  _classifyBibliography(ref) {
77
- // 1. cite:key form: try the bibliography registry.
78
53
  if (ref.citation) {
79
- const bioRecord = this._tryBibliography(ref.citation.ref);
80
- if (bioRecord) return 'internal-citation';
81
- return 'self-contained-citation';
54
+ return resolveBibliographyRecord(ref.citation.ref, this.registry)
55
+ ? 'internal-citation'
56
+ : 'self-contained-citation';
82
57
  }
83
- // 2. URI form: try the bibliography registry, then the
84
- // resolution's datasetId (if it's a concept URI), else null.
85
58
  if (ref.uri) {
86
- const bioRecord = this._tryBibliography(ref.resolution);
87
- if (bioRecord) return 'internal-citation';
88
- return 'external-citation';
59
+ return resolveBibliographyRecord(ref.resolution, this.registry)
60
+ ? 'internal-citation'
61
+ : 'external-citation';
89
62
  }
90
63
  return 'unresolved-citation';
91
64
  }
@@ -94,13 +67,9 @@ export class ReferenceClassifier {
94
67
  return 'typed-ref';
95
68
  }
96
69
 
97
- _tryBibliography(citationRef) {
98
- if (!citationRef?.source || !citationRef?.id) return null;
99
- const bioColl = this.registry[`bibliography:${citationRef.source}`]?.concepts;
100
- if (!bioColl) return null;
101
- if (citationRef.version) {
102
- return bioColl.byIdAnd(citationRef.id, citationRef.version) ?? null;
103
- }
104
- return bioColl.byId(citationRef.id) ?? null;
70
+ _classifyNonVerbal(ref) {
71
+ return findNonVerbalEntity(ref, this.registry)
72
+ ? 'internal-citation'
73
+ : 'external-citation';
105
74
  }
106
75
  }
@@ -0,0 +1,66 @@
1
+ import { DATASET_ASSETS } from '../dataset-asset.js';
2
+
3
+ export class AssetIndex {
4
+ constructor() {
5
+ this._paths = new Set();
6
+ }
7
+
8
+ get paths() {
9
+ return [...this._paths].sort();
10
+ }
11
+
12
+ get size() {
13
+ return this._paths.size;
14
+ }
15
+
16
+ register(path) {
17
+ if (path == null) return;
18
+ this._paths.add(this._normalize(path));
19
+ }
20
+
21
+ has(path) {
22
+ if (path == null) return false;
23
+ return this._paths.has(this._normalize(path));
24
+ }
25
+
26
+ [Symbol.iterator]() {
27
+ return this._paths[Symbol.iterator]();
28
+ }
29
+
30
+ _normalize(path) {
31
+ return String(path).replace(/^\//, '');
32
+ }
33
+
34
+ static async fromGcrPackage(pkg) {
35
+ const index = new AssetIndex();
36
+ const names = await pkg.imageFileNames();
37
+ for (const name of names) {
38
+ index.register(name);
39
+ }
40
+ return index;
41
+ }
42
+
43
+ static fromDirectory(datasetPath, fs) {
44
+ const index = new AssetIndex();
45
+ const imagesAsset = DATASET_ASSETS.find(
46
+ a => a.type === 'directory' && a.path === 'images');
47
+ if (!imagesAsset) return index;
48
+
49
+ const imagesDir = `${datasetPath}/${imagesAsset.path}`;
50
+ _walkDir(fs, imagesDir, imagesAsset.path, index);
51
+ return index;
52
+ }
53
+ }
54
+
55
+ function _walkDir(fs, dirPath, relativePrefix, index) {
56
+ if (!fs.existsSync(dirPath)) return;
57
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
58
+ const fullPath = `${dirPath}/${entry.name}`;
59
+ const relPath = `${relativePrefix}/${entry.name}`;
60
+ if (entry.isDirectory()) {
61
+ _walkDir(fs, fullPath, relPath, index);
62
+ } else {
63
+ index.register(relPath);
64
+ }
65
+ }
66
+ }