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.
- package/package.json +1 -1
- package/src/concept-parser.js +7 -1
- package/src/concept-reader.js +1 -1
- package/src/dataset-asset.js +3 -0
- package/src/entity-directory.js +43 -0
- package/src/gcr-reader.js +53 -5
- package/src/gcr-writer.js +3 -1
- package/src/index.d.ts +40 -7
- package/src/index.js +19 -2
- package/src/managed-concept-collection.js +11 -2
- package/src/models/base.js +14 -0
- package/src/models/bibliography-data.js +43 -0
- package/src/models/bibliography-entry.js +26 -0
- package/src/models/concept.js +27 -5
- package/src/models/designation.js +2 -14
- package/src/models/figure.js +71 -0
- package/src/models/formula.js +21 -0
- package/src/models/index.js +12 -1
- package/src/models/localized-concept.js +0 -14
- package/src/models/localized-string.js +19 -0
- package/src/models/non-verb-rep.js +15 -13
- package/src/models/non-verbal-entity.js +39 -0
- package/src/models/non-verbal-reference.js +35 -0
- package/src/models/non-verbal-references.js +16 -0
- package/src/models/registrable.js +25 -0
- package/src/models/shared-non-verbal-entity.js +28 -0
- package/src/models/table.js +21 -0
- package/src/reference-mention.js +19 -3
- package/src/reference-resolver.js +135 -184
- package/src/render-classification.js +19 -50
- package/src/validators/asset-index.js +66 -0
- package/src/validators/concept-validator.js +18 -33
- package/src/validators/index.js +5 -1
- package/src/validators/v3-rules.js +179 -43
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
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;
|
|
112
|
-
const content =
|
|
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;
|
|
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;
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
247
|
+
}
|
|
252
248
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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':
|
|
35
|
-
case 'dataset':
|
|
14
|
+
case 'concept': return this._classifyConcept(ref);
|
|
15
|
+
case 'dataset': return this._classifyDataset(ref);
|
|
36
16
|
case 'bibliography': return this._classifyBibliography(ref);
|
|
37
|
-
case '
|
|
38
|
-
case '
|
|
39
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
}
|