@glossarist/concept-browser 0.7.35 → 0.7.37
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 +2 -2
- package/scripts/build-edges.js +16 -8
- package/scripts/generate-data.mjs +284 -86
- package/src/__tests__/citation-display.test.ts +165 -3
- package/src/__tests__/cite-ref.test.ts +112 -0
- package/src/__tests__/concept-detail-interaction.test.ts +1 -1
- package/src/__tests__/{math.test.ts → content-renderer.test.ts} +113 -29
- package/src/__tests__/escape.test.ts +76 -0
- package/src/__tests__/graph-data-source.test.ts +155 -0
- package/src/__tests__/model-bridge-bridges.test.ts +150 -0
- package/src/__tests__/model-bridge-citation.test.ts +163 -0
- package/src/__tests__/reference-resolver-cite.test.ts +122 -0
- package/src/__tests__/reference-resolver.test.ts +12 -7
- package/src/__tests__/resolve-view.test.ts +1 -1
- package/src/__tests__/sidebar-nav-highlighting.test.ts +178 -0
- package/src/__tests__/source-refs.test.ts +9 -6
- package/src/__tests__/test-helpers.ts +20 -0
- package/src/__tests__/uri-router.test.ts +39 -12
- package/src/adapters/DatasetAdapter.ts +12 -0
- package/src/adapters/GraphDataSource.ts +3 -3
- package/src/adapters/ReferenceResolver.ts +85 -55
- package/src/adapters/UriRouter.ts +82 -10
- package/src/adapters/factory.ts +34 -10
- package/src/adapters/model-bridge.ts +121 -71
- package/src/adapters/types.ts +3 -0
- package/src/components/AppSidebar.vue +7 -4
- package/src/components/CitationDisplay.vue +86 -30
- package/src/components/ConceptDetail.vue +6 -4
- package/src/components/LanguageDetail.vue +6 -6
- package/src/composables/use-concept-content.ts +8 -8
- package/src/composables/use-render-options.ts +1 -1
- package/src/graph/GraphEngine.ts +3 -3
- package/src/stores/vocabulary.ts +2 -2
- package/src/utils/content-renderer.ts +312 -0
- package/src/utils/markdown-lite.ts +2 -2
- package/src/utils/math.ts +0 -189
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.37",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"autoprefixer": "^10.4.21",
|
|
26
26
|
"d3": "^7.9.0",
|
|
27
27
|
"favicons": "^7.2.0",
|
|
28
|
-
"glossarist": "^0.3.
|
|
28
|
+
"glossarist": "^0.3.7",
|
|
29
29
|
"js-yaml": "^4.1.0",
|
|
30
30
|
"pinia": "^2.3.1",
|
|
31
31
|
"postcss": "^8.5.3",
|
package/scripts/build-edges.js
CHANGED
|
@@ -31,14 +31,16 @@ function extractReferences(concept, registerId) {
|
|
|
31
31
|
if (lc['gl:references']) {
|
|
32
32
|
for (const ref of lc['gl:references']) {
|
|
33
33
|
if (ref['@id'] && ref['@id'] !== sourceUri) {
|
|
34
|
-
|
|
34
|
+
const edge = {
|
|
35
35
|
source: sourceUri,
|
|
36
36
|
target: ref['@id'],
|
|
37
|
-
type: 'references',
|
|
37
|
+
type: ref['@id'].startsWith('cite:') ? 'citation' : 'references',
|
|
38
38
|
label: ref['gl:term'] || undefined,
|
|
39
39
|
register: registerId,
|
|
40
40
|
lang,
|
|
41
|
-
}
|
|
41
|
+
};
|
|
42
|
+
if (ref['gl:sourceId']) edge.sourceId = ref['gl:sourceId'];
|
|
43
|
+
edges.push(edge);
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -263,7 +265,7 @@ const datasets = readdirSync(DATA_DIR).filter(f => {
|
|
|
263
265
|
}
|
|
264
266
|
});
|
|
265
267
|
|
|
266
|
-
// Build
|
|
268
|
+
// Build URI→datasetId prefix map from all manifests
|
|
267
269
|
const urnMap = new Map();
|
|
268
270
|
const manifestCache = new Map();
|
|
269
271
|
for (const ds of datasets) {
|
|
@@ -271,14 +273,17 @@ for (const ds of datasets) {
|
|
|
271
273
|
try {
|
|
272
274
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
273
275
|
manifestCache.set(ds, manifest);
|
|
274
|
-
if (manifest.datasetUri)
|
|
276
|
+
if (manifest.datasetUri) {
|
|
277
|
+
const base = manifest.datasetUri.endsWith('*') ? manifest.datasetUri.slice(0, -1) : manifest.datasetUri;
|
|
278
|
+
if (base) urnMap.set(base, ds);
|
|
279
|
+
}
|
|
275
280
|
for (const alias of manifest.uriAliases ?? []) {
|
|
276
281
|
const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
|
|
277
|
-
if (base
|
|
282
|
+
if (base) urnMap.set(base, ds);
|
|
278
283
|
}
|
|
279
284
|
} catch {}
|
|
280
285
|
}
|
|
281
|
-
console.log(`
|
|
286
|
+
console.log(`URI resolution map: ${[...urnMap.entries()].map(([k,v]) => `${k}→${v}`).join(', ')}\n`);
|
|
282
287
|
|
|
283
288
|
const allDatasetEdges = new Map();
|
|
284
289
|
const allSourceRefs = [];
|
|
@@ -310,7 +315,10 @@ for (const [ds, manifest] of manifestCache) {
|
|
|
310
315
|
for (const alias of manifest.refAliases ?? []) {
|
|
311
316
|
knownSourceStrings.add(alias);
|
|
312
317
|
}
|
|
313
|
-
if (manifest.datasetUri)
|
|
318
|
+
if (manifest.datasetUri) {
|
|
319
|
+
const base = manifest.datasetUri.endsWith('*') ? manifest.datasetUri.slice(0, -1) : manifest.datasetUri;
|
|
320
|
+
knownSourceStrings.add(base);
|
|
321
|
+
}
|
|
314
322
|
for (const alias of manifest.uriAliases ?? []) {
|
|
315
323
|
const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
|
|
316
324
|
knownSourceStrings.add(base);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
|
-
import { naturalSort, Register } from 'glossarist';
|
|
4
|
+
import { naturalSort, Register, parseMention } from 'glossarist';
|
|
5
5
|
import { loadSiteConfig } from './load-site-config.mjs';
|
|
6
6
|
|
|
7
7
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
@@ -19,6 +19,11 @@ function readYaml(filePath) {
|
|
|
19
19
|
return yaml.load(fs.readFileSync(filePath, 'utf8'));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Strip HTML tags and normalize whitespace for plain-text display. */
|
|
23
|
+
function stripHtml(s) {
|
|
24
|
+
return s.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
function loadConceptFile(filePath) {
|
|
23
28
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
24
29
|
const docs = yaml.loadAll(content, null, { schema: yaml.DEFAULT_SCHEMA });
|
|
@@ -136,36 +141,43 @@ function defsToJsonLd(defs) {
|
|
|
136
141
|
.filter(d => d['gl:content']);
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
function refToJsonLd(ref) {
|
|
145
|
+
if (!ref) return undefined;
|
|
146
|
+
const refObj = { '@type': 'gl:Ref' };
|
|
147
|
+
if (typeof ref === 'string') {
|
|
148
|
+
refObj['gl:source'] = ref;
|
|
149
|
+
} else {
|
|
150
|
+
if (ref.source) refObj['gl:source'] = ref.source;
|
|
151
|
+
if (ref.id) refObj['gl:id'] = ref.id;
|
|
152
|
+
if (ref.version) refObj['gl:version'] = ref.version;
|
|
153
|
+
}
|
|
154
|
+
return refObj;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function localityToJsonLd(loc) {
|
|
158
|
+
if (!loc) return undefined;
|
|
159
|
+
const locObj = {};
|
|
160
|
+
if (loc.type) locObj['gl:localityType'] = loc.type;
|
|
161
|
+
if (loc.reference_from) locObj['gl:referenceFrom'] = loc.reference_from;
|
|
162
|
+
if (loc.referenceFrom) locObj['gl:referenceFrom'] = loc.referenceFrom;
|
|
163
|
+
if (loc.reference_to) locObj['gl:referenceTo'] = loc.reference_to;
|
|
164
|
+
if (loc.referenceTo) locObj['gl:referenceTo'] = loc.referenceTo;
|
|
165
|
+
return Object.keys(locObj).length > 0 ? locObj : undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
139
168
|
function sourcesToJsonLd(sources) {
|
|
140
169
|
if (!sources || !Array.isArray(sources)) return [];
|
|
141
170
|
return sources.map(s => {
|
|
142
171
|
const doc = { '@type': 'gl:ConceptSource' };
|
|
172
|
+
if (s.id) doc['gl:id'] = s.id;
|
|
143
173
|
if (s.type) doc['gl:sourceType'] = s.type;
|
|
144
174
|
if (s.status) doc['gl:sourceStatus'] = s.status;
|
|
145
175
|
if (s.origin) {
|
|
146
176
|
const origin = { '@type': 'gl:Citation' };
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
refObj['gl:source'] = ref;
|
|
152
|
-
} else {
|
|
153
|
-
if (ref.source) refObj['gl:source'] = ref.source;
|
|
154
|
-
if (ref.id) refObj['gl:id'] = ref.id;
|
|
155
|
-
if (ref.version) refObj['gl:version'] = ref.version;
|
|
156
|
-
}
|
|
157
|
-
origin['gl:ref'] = refObj;
|
|
158
|
-
}
|
|
159
|
-
if (s.origin.locality) {
|
|
160
|
-
const loc = s.origin.locality;
|
|
161
|
-
const locObj = {};
|
|
162
|
-
if (loc.type) locObj['gl:localityType'] = loc.type;
|
|
163
|
-
if (loc.reference_from) locObj['gl:referenceFrom'] = loc.reference_from;
|
|
164
|
-
if (loc.referenceFrom) locObj['gl:referenceFrom'] = loc.referenceFrom;
|
|
165
|
-
if (loc.reference_to) locObj['gl:referenceTo'] = loc.reference_to;
|
|
166
|
-
if (loc.referenceTo) locObj['gl:referenceTo'] = loc.referenceTo;
|
|
167
|
-
origin['gl:locality'] = locObj;
|
|
168
|
-
}
|
|
177
|
+
const ref = refToJsonLd(s.origin.ref);
|
|
178
|
+
if (ref) origin['gl:ref'] = ref;
|
|
179
|
+
const loc = localityToJsonLd(s.origin.locality);
|
|
180
|
+
if (loc) origin['gl:locality'] = loc;
|
|
169
181
|
if (s.origin.link) origin['gl:link'] = s.origin.link;
|
|
170
182
|
doc['gl:origin'] = origin;
|
|
171
183
|
}
|
|
@@ -176,7 +188,12 @@ function sourcesToJsonLd(sources) {
|
|
|
176
188
|
function refsToJsonLd(refs, refMaps) {
|
|
177
189
|
if (!refs || !Array.isArray(refs)) return [];
|
|
178
190
|
return refs.map(r => {
|
|
179
|
-
if (r.id)
|
|
191
|
+
if (r.id) {
|
|
192
|
+
const ref = { '@id': r.id, 'gl:term': r.term };
|
|
193
|
+
if (r.sourceId) ref['gl:sourceId'] = r.sourceId;
|
|
194
|
+
if (r.citation) ref['gl:citation'] = citationToJsonLd(r.citation);
|
|
195
|
+
return ref;
|
|
196
|
+
}
|
|
180
197
|
if (r.term && refMaps) {
|
|
181
198
|
const uri = resolveRefUri(r.term, refMaps);
|
|
182
199
|
if (uri) return { '@id': uri, 'gl:term': r.term };
|
|
@@ -185,34 +202,70 @@ function refsToJsonLd(refs, refMaps) {
|
|
|
185
202
|
}).filter(r => r['@id']);
|
|
186
203
|
}
|
|
187
204
|
|
|
188
|
-
function
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
function citationToJsonLd(citation) {
|
|
206
|
+
const obj = {};
|
|
207
|
+
const ref = refToJsonLd(citation.ref);
|
|
208
|
+
if (ref) obj['gl:ref'] = ref;
|
|
209
|
+
const loc = localityToJsonLd(citation.locality);
|
|
210
|
+
if (loc) obj['gl:locality'] = loc;
|
|
211
|
+
if (citation.link) obj['gl:link'] = citation.link;
|
|
212
|
+
return obj;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildPatternIndex(datasets, registerCache) {
|
|
216
|
+
const entries = [];
|
|
217
|
+
|
|
218
|
+
for (const ds of datasets) {
|
|
219
|
+
const reg = registerCache[ds.id] || null;
|
|
220
|
+
const patterns = new Set();
|
|
221
|
+
|
|
222
|
+
// Site-config patterns (primary)
|
|
223
|
+
if (ds.uri) patterns.add(ds.uri);
|
|
224
|
+
for (const alias of ds.uriAliases || []) patterns.add(alias);
|
|
225
|
+
|
|
226
|
+
// Register.yaml patterns (supplementary)
|
|
227
|
+
if (reg) {
|
|
228
|
+
if (reg.urn && reg.urn.endsWith('*')) patterns.add(reg.urn);
|
|
229
|
+
for (const alias of reg.urnAliases || []) patterns.add(alias);
|
|
197
230
|
}
|
|
231
|
+
|
|
232
|
+
for (const pattern of patterns) {
|
|
233
|
+
if (!pattern.endsWith('*')) continue;
|
|
234
|
+
const prefix = pattern.slice(0, -1);
|
|
235
|
+
if (prefix) entries.push({ prefix, datasetId: ds.id });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sort longest prefix first for correct longest-prefix matching
|
|
240
|
+
entries.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
241
|
+
|
|
242
|
+
function resolve(uri) {
|
|
243
|
+
for (const { prefix, datasetId } of entries) {
|
|
244
|
+
if (uri.startsWith(prefix)) {
|
|
245
|
+
return { datasetId, conceptId: uri.slice(prefix.length) };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
198
249
|
}
|
|
250
|
+
|
|
251
|
+
return { resolve, entries };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveRefUri(term, refMaps) {
|
|
255
|
+
const resolved = refMaps.patternIndex.resolve(term);
|
|
256
|
+
if (resolved) return `${refMaps.uriBase}/${resolved.datasetId}/concept/${resolved.conceptId}`;
|
|
257
|
+
|
|
199
258
|
const ievMatch = term.match(/^IEV:(\d+[-\d]+)$/);
|
|
200
259
|
if (ievMatch) {
|
|
201
260
|
const dsId = refMaps.refPrefixMap['IEV'];
|
|
202
|
-
if (dsId) return `${
|
|
261
|
+
if (dsId) return `${refMaps.uriBase}/${dsId}/concept/${ievMatch[1]}`;
|
|
203
262
|
}
|
|
204
263
|
return null;
|
|
205
264
|
}
|
|
206
265
|
|
|
207
|
-
function buildRefMaps(config) {
|
|
266
|
+
function buildRefMaps(config, registerCache) {
|
|
208
267
|
const refPrefixMap = {};
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
for (const ds of config.datasets) {
|
|
212
|
-
const uri = ds.uri || '';
|
|
213
|
-
const urnMatch = uri.match(/^urn:iso:std:iso:(\d+):\*$/);
|
|
214
|
-
if (urnMatch) urnStandardMap[urnMatch[1]] = ds.id;
|
|
215
|
-
}
|
|
268
|
+
const patternIndex = buildPatternIndex(config.datasets, registerCache);
|
|
216
269
|
|
|
217
270
|
for (const route of config.routing || []) {
|
|
218
271
|
if (route.uri && route.uri.includes('iec') && route.uri.includes('60050')) {
|
|
@@ -223,56 +276,172 @@ function buildRefMaps(config) {
|
|
|
223
276
|
|
|
224
277
|
const xref = config.crossReferences || {};
|
|
225
278
|
if (xref.refPrefixMap) Object.assign(refPrefixMap, xref.refPrefixMap);
|
|
226
|
-
if (xref.urnStandardMap) Object.assign(urnStandardMap, xref.urnStandardMap);
|
|
227
279
|
|
|
228
280
|
const uriBase = config.uriBase || `https://${config.domain}`;
|
|
229
|
-
return {
|
|
281
|
+
return { patternIndex, refPrefixMap, uriBase, register: null };
|
|
230
282
|
}
|
|
231
283
|
|
|
232
|
-
function extractInlineRefs(localizedData, refMaps) {
|
|
233
|
-
const refs = [];
|
|
234
|
-
const texts = [];
|
|
235
|
-
const { refPrefixMap, urnStandardMap, uriBase } = refMaps;
|
|
236
284
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
285
|
+
// ── Mention handlers (OCP: add new mention kinds by adding handlers) ─────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Resolve an IEV:NNN-NN-NN display form to a concept URI.
|
|
289
|
+
*/
|
|
290
|
+
function resolveIevRef(display, term, refPrefixMap, uriBase) {
|
|
291
|
+
if (!display.startsWith('IEV:')) return null;
|
|
292
|
+
const datasetId = refPrefixMap['IEV'];
|
|
293
|
+
if (!datasetId) return null;
|
|
294
|
+
return { id: `${uriBase}/${datasetId}/concept/${display.slice(4)}`, term };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Resolve a URI identifier via the pattern index.
|
|
299
|
+
*/
|
|
300
|
+
function resolvePatternRef(identifier, display, refPrefixMap, patternIndex, uriBase) {
|
|
301
|
+
const r = patternIndex.resolve(identifier);
|
|
302
|
+
if (!r) return null;
|
|
303
|
+
return { id: `${uriBase}/${r.datasetId}/concept/${r.conceptId}`, term: display };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Handle a cite-ref mention: links to a ConceptSource entry.
|
|
308
|
+
*/
|
|
309
|
+
function handleCiteRef(parsed, allSources) {
|
|
310
|
+
const sourceEntry = allSources.find(s => s.id === parsed.key);
|
|
311
|
+
if (sourceEntry) {
|
|
312
|
+
return {
|
|
313
|
+
id: `cite:${sourceEntry.id}`,
|
|
314
|
+
term: parsed.label || sourceEntry.origin?.toString?.() || sourceEntry.id,
|
|
315
|
+
sourceId: sourceEntry.id,
|
|
316
|
+
citation: sourceEntry.origin || null,
|
|
317
|
+
};
|
|
246
318
|
}
|
|
247
|
-
|
|
319
|
+
return {
|
|
320
|
+
id: `cite:${parsed.key}`,
|
|
321
|
+
term: parsed.label || parsed.key,
|
|
322
|
+
sourceId: parsed.key,
|
|
323
|
+
citation: null,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Handle a numeric mention: bare concept ID in same dataset.
|
|
329
|
+
*/
|
|
330
|
+
function handleNumeric(parsed, register, uriBase) {
|
|
331
|
+
if (!register) return null;
|
|
332
|
+
const term = parsed.label ?? parsed.id;
|
|
333
|
+
return { id: `${uriBase}/${register}/concept/${parsed.id}`, term };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Handle a designation mention (glossarist >= 0.3.7):
|
|
338
|
+
* {{designation,render term}} — resolve designation to concept ID in same dataset.
|
|
339
|
+
* Falls back to the raw designation as the concept ID if no lookup table exists.
|
|
340
|
+
*/
|
|
341
|
+
function handleDesignation(parsed, refMaps) {
|
|
342
|
+
const register = refMaps.register;
|
|
343
|
+
if (!register) return null;
|
|
344
|
+
const designation = parsed.id;
|
|
345
|
+
const display = parsed.label ?? designation;
|
|
346
|
+
const conceptId = refMaps.designationLookup?.get(designation.toLowerCase());
|
|
347
|
+
return {
|
|
348
|
+
id: `${refMaps.uriBase}/${register}/concept/${conceptId ?? designation}`,
|
|
349
|
+
term: display,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
248
352
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Handle an unresolved double-brace mention: try two-arg form.
|
|
355
|
+
* Format: {{conceptId, displayTerm}} — concept ID first, render term last.
|
|
356
|
+
*/
|
|
357
|
+
function handleUnresolved(body, refMaps) {
|
|
358
|
+
const commaMatch = body.match(/^([^,}]+),\s*(.+)$/);
|
|
359
|
+
if (!commaMatch) return null;
|
|
360
|
+
const identifier = commaMatch[1].trim();
|
|
361
|
+
const display = commaMatch[2].trim();
|
|
362
|
+
|
|
363
|
+
// IEV shortform: {{IEV:shortform, display_term}}
|
|
364
|
+
const iev = resolveIevRef(identifier, display, refMaps.refPrefixMap, refMaps.uriBase);
|
|
365
|
+
if (iev) return iev;
|
|
366
|
+
|
|
367
|
+
// URI pattern match
|
|
368
|
+
const pattern = resolvePatternRef(identifier, display, refMaps.refPrefixMap, refMaps.patternIndex, refMaps.uriBase);
|
|
369
|
+
if (pattern) return pattern;
|
|
370
|
+
|
|
371
|
+
// Same-dataset: {{conceptId, displayTerm}} where conceptId is numeric/X.Y
|
|
372
|
+
const register = refMaps.register;
|
|
373
|
+
if (register && (/^\d/.test(identifier) || /^[A-Z]\.\d/.test(identifier))) {
|
|
374
|
+
return { id: `${refMaps.uriBase}/${register}/concept/${identifier}`, term: display };
|
|
252
375
|
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
253
378
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
379
|
+
// ── Inline reference extraction ───────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
function collectTextContent(localizedData) {
|
|
382
|
+
const texts = [];
|
|
383
|
+
const textFields = ['definition', 'notes', 'examples'];
|
|
384
|
+
for (const field of textFields) {
|
|
385
|
+
const items = localizedData[field];
|
|
386
|
+
if (!items) continue;
|
|
387
|
+
const arr = Array.isArray(items) ? items : [items];
|
|
388
|
+
for (const item of arr) {
|
|
389
|
+
texts.push(typeof item === 'string' ? item : (item.content || ''));
|
|
390
|
+
}
|
|
257
391
|
}
|
|
392
|
+
return texts.join(' ');
|
|
393
|
+
}
|
|
258
394
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
395
|
+
function extractInlineRefs(localizedData, refMaps, conceptSources = []) {
|
|
396
|
+
const refs = [];
|
|
397
|
+
const { refPrefixMap, patternIndex, uriBase } = refMaps;
|
|
398
|
+
const fullText = collectTextContent(localizedData);
|
|
399
|
+
const allSources = [...(localizedData.sources || []), ...conceptSources];
|
|
400
|
+
|
|
401
|
+
// Single-brace mentions: {uri,display} (not {{...}})
|
|
402
|
+
for (const m of fullText.matchAll(/\{([^,}]+),([^,}]+)(?:,([^}]+))?\}/g)) {
|
|
403
|
+
const identifier = m[1].trim();
|
|
404
|
+
const display = m[2].trim();
|
|
405
|
+
const altDisplay = (m[3] || '').trim();
|
|
406
|
+
if (!identifier || !display) continue;
|
|
407
|
+
|
|
408
|
+
const iev = resolveIevRef(display, identifier, refPrefixMap, uriBase);
|
|
409
|
+
if (iev) { refs.push(iev); continue; }
|
|
410
|
+
|
|
411
|
+
const pattern = resolvePatternRef(identifier, altDisplay || display, refPrefixMap, patternIndex, uriBase);
|
|
412
|
+
if (pattern) { refs.push(pattern); }
|
|
262
413
|
}
|
|
263
414
|
|
|
264
|
-
//
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
415
|
+
// Double-brace mentions: dispatched by parseMention kind
|
|
416
|
+
for (const m of fullText.matchAll(/\{\{([^{}]+?)\}\}/g)) {
|
|
417
|
+
const body = m[1];
|
|
418
|
+
const parsed = parseMention(body);
|
|
419
|
+
|
|
420
|
+
let ref = null;
|
|
421
|
+
if (parsed.kind === 'cite-ref') {
|
|
422
|
+
ref = handleCiteRef(parsed, allSources);
|
|
423
|
+
} else if (parsed.kind === 'numeric') {
|
|
424
|
+
ref = handleNumeric(parsed, refMaps.register, uriBase);
|
|
425
|
+
} else if (parsed.kind === 'urn-ref') {
|
|
426
|
+
// {{urn:...,render term}} — resolve URN via pattern index (cross-dataset)
|
|
427
|
+
const uri = parsed.uri;
|
|
428
|
+
const term = parsed.label ?? uri;
|
|
429
|
+
const pattern = refMaps.patternIndex.resolve(uri);
|
|
430
|
+
if (pattern) {
|
|
431
|
+
ref = { id: `${refMaps.uriBase}/${pattern.datasetId}/concept/${pattern.conceptId}`, term };
|
|
432
|
+
} else {
|
|
433
|
+
ref = { id: uri, term };
|
|
434
|
+
}
|
|
435
|
+
} else if (parsed.kind === 'designation') {
|
|
436
|
+
// {{designation,render term}} — same-dataset designation reference
|
|
437
|
+
ref = handleDesignation(parsed, refMaps);
|
|
438
|
+
} else {
|
|
439
|
+
ref = handleUnresolved(body, refMaps);
|
|
273
440
|
}
|
|
441
|
+
if (ref) refs.push(ref);
|
|
274
442
|
}
|
|
275
443
|
|
|
444
|
+
// Deduplicate by id
|
|
276
445
|
const seen = new Set();
|
|
277
446
|
return refs.filter(r => {
|
|
278
447
|
if (seen.has(r.id)) return false;
|
|
@@ -333,7 +502,7 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
333
502
|
if (lc.references && lc.references.length > 0) {
|
|
334
503
|
lDoc['gl:references'] = refsToJsonLd(lc.references, refMaps);
|
|
335
504
|
} else if (refMaps) {
|
|
336
|
-
const inlineRefs = extractInlineRefs(lc, refMaps);
|
|
505
|
+
const inlineRefs = extractInlineRefs(lc, refMaps, conceptYaml._sources);
|
|
337
506
|
if (inlineRefs.length > 0) {
|
|
338
507
|
lDoc['gl:references'] = refsToJsonLd(inlineRefs, refMaps);
|
|
339
508
|
}
|
|
@@ -632,7 +801,28 @@ function processDataset(dir, register, opts) {
|
|
|
632
801
|
const langTermCounts = {};
|
|
633
802
|
const langDefCounts = {};
|
|
634
803
|
const availableFormats = ['ttl', 'jsonld', 'yaml', 'tbx'];
|
|
635
|
-
|
|
804
|
+
|
|
805
|
+
// Pre-scan: build designation → concept ID lookup for same-dataset designation refs
|
|
806
|
+
const designationLookup = new Map();
|
|
807
|
+
for (const file of files) {
|
|
808
|
+
try {
|
|
809
|
+
const conceptYaml = loadConceptFile(path.join(dir, file));
|
|
810
|
+
if (!conceptYaml?.termid) continue;
|
|
811
|
+
const termid = String(conceptYaml.termid);
|
|
812
|
+
for (const lang of Object.keys(conceptYaml)) {
|
|
813
|
+
const lc = conceptYaml[lang];
|
|
814
|
+
if (!lc || typeof lc !== 'object' || !Array.isArray(lc.terms)) continue;
|
|
815
|
+
for (const term of lc.terms) {
|
|
816
|
+
const designation = term.designation;
|
|
817
|
+
if (typeof designation === 'string' && designation && !designationLookup.has(designation.toLowerCase())) {
|
|
818
|
+
designationLookup.set(designation.toLowerCase(), termid);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
} catch {}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const dsRefMaps = { ...refMaps, register, designationLookup };
|
|
636
826
|
|
|
637
827
|
for (let i = 0; i < files.length; i++) {
|
|
638
828
|
const file = files[i];
|
|
@@ -707,15 +897,22 @@ function processDataset(dir, register, opts) {
|
|
|
707
897
|
}));
|
|
708
898
|
|
|
709
899
|
// Strip HTML from index summary for text display
|
|
710
|
-
const plainSummary = summary.map(c =>
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
900
|
+
const plainSummary = summary.map(c => {
|
|
901
|
+
const designations = {};
|
|
902
|
+
for (const [lang, term] of Object.entries(c.designations)) {
|
|
903
|
+
if (term) designations[lang] = stripHtml(term);
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
...c,
|
|
907
|
+
designations,
|
|
908
|
+
eng: stripHtml(c.eng),
|
|
909
|
+
};
|
|
910
|
+
});
|
|
714
911
|
|
|
715
912
|
const graphNodeEntries = concepts.map(c => {
|
|
716
913
|
const cleanDesignations = {};
|
|
717
914
|
for (const [l, t] of Object.entries(c.designations)) {
|
|
718
|
-
if (t) cleanDesignations[l] = t
|
|
915
|
+
if (t) cleanDesignations[l] = stripHtml(t);
|
|
719
916
|
}
|
|
720
917
|
return [c.id, cleanDesignations, c.status];
|
|
721
918
|
});
|
|
@@ -845,12 +1042,11 @@ function processDataset(dir, register, opts) {
|
|
|
845
1042
|
console.log('Generating Glossarist vocabulary browser data...\n');
|
|
846
1043
|
|
|
847
1044
|
const { config } = loadSiteConfig();
|
|
848
|
-
const refMaps = buildRefMaps(config);
|
|
849
1045
|
const counts = {};
|
|
850
1046
|
const registry = [];
|
|
851
1047
|
const registerCache = {};
|
|
852
1048
|
|
|
853
|
-
// Pre-load all register.yaml files
|
|
1049
|
+
// Pre-load all register.yaml files (needed before buildRefMaps for URI pattern indexing)
|
|
854
1050
|
for (const ds of config.datasets) {
|
|
855
1051
|
const registerDir = path.join(ROOT, '.datasets', ds.id);
|
|
856
1052
|
const registerYamlPath = path.join(registerDir, 'register.yaml');
|
|
@@ -864,6 +1060,8 @@ for (const ds of config.datasets) {
|
|
|
864
1060
|
}
|
|
865
1061
|
}
|
|
866
1062
|
|
|
1063
|
+
const refMaps = buildRefMaps(config, registerCache);
|
|
1064
|
+
|
|
867
1065
|
for (let i = 0; i < config.datasets.length; i++) {
|
|
868
1066
|
const ds = config.datasets[i];
|
|
869
1067
|
|
|
@@ -937,8 +1135,8 @@ writeJson(path.join(PUBLIC, 'datasets.json'), registry);
|
|
|
937
1135
|
const configuredIds = new Set(config.datasets.map(d => d.id));
|
|
938
1136
|
if (fs.existsSync(DATA)) {
|
|
939
1137
|
for (const entry of fs.readdirSync(DATA)) {
|
|
940
|
-
if (!configuredIds.has(entry)) {
|
|
941
1138
|
const stalePath = path.join(DATA, entry);
|
|
1139
|
+
if (!configuredIds.has(entry) && fs.statSync(stalePath).isDirectory()) {
|
|
942
1140
|
fs.rmSync(stalePath, { recursive: true, force: true });
|
|
943
1141
|
console.log(` Removed stale data directory: ${entry}`);
|
|
944
1142
|
}
|