@glossarist/concept-browser 0.7.114 → 0.7.115
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/README.md +45 -2
- package/cli/index.mjs +15 -15
- package/cli/index.ts +9 -11
- package/data/concept-model/shapes/glossarist.concept.yaml.schema.json +255 -0
- package/package.json +14 -12
- package/scripts/__tests__/{bibliography.test.mjs → bibliography.test.ts} +1 -1
- package/scripts/__tests__/{cli-script-contract.test.mjs → cli-script-contract.test.ts} +3 -3
- package/scripts/__tests__/{concept-groups.test.mjs → concept-groups.test.ts} +1 -1
- package/scripts/__tests__/{doctor.test.mjs → doctor.test.ts} +1 -1
- package/scripts/__tests__/{fetch-datasets.test.mjs → fetch-datasets.test.ts} +1 -1
- package/scripts/__tests__/{first-non-empty.test.mjs → first-non-empty.test.ts} +1 -1
- package/scripts/__tests__/{process-about-pages.test.mjs → process-about-pages.test.ts} +1 -1
- package/scripts/{bridge-to-astro.mjs → bridge-to-astro.ts} +2 -4
- package/scripts/{build-edges.js → build-edges.ts} +11 -13
- package/scripts/{check-syntax.mjs → check-syntax.ts} +20 -13
- package/scripts/{fetch-datasets.mjs → fetch-datasets.ts} +2 -2
- package/scripts/{generate-data.mjs → generate-data.ts} +73 -460
- package/scripts/{generate-ontology-schema.mjs → generate-ontology-schema.ts} +2 -2
- package/scripts/lib/{build-cache.mjs → build-cache.ts} +3 -2
- package/scripts/lib/concept-formats.ts +206 -0
- package/scripts/lib/concept-uri.ts +14 -0
- package/scripts/lib/{dataset-turtle.mjs → dataset-turtle.ts} +4 -3
- package/scripts/lib/harmonize-concept.ts +86 -0
- package/scripts/lib/jsonld-emitter.ts +169 -0
- package/scripts/lib/{local-path-safety.mjs → local-path-safety.ts} +6 -2
- package/scripts/lib/yaml-types.ts +262 -0
- package/scripts/{process-about-pages.mjs → process-about-pages.ts} +1 -1
- package/scripts/{smoke.mjs → smoke.ts} +4 -3
- package/scripts/{validate-shacl.mjs → validate-shacl.ts} +5 -5
- package/scripts/validate-yaml.ts +144 -0
- package/src/__tests__/extract-source-refs.test.ts +2 -3
- package/src/__tests__/perf/serialization-perf.test.ts +5 -5
- package/src/__tests__/scripts/agents-turtle.test.ts +1 -1
- package/src/__tests__/scripts/bibliography-turtle.test.ts +1 -1
- package/src/__tests__/scripts/build-activity-turtle.test.ts +1 -1
- package/src/__tests__/scripts/build-cache.test.ts +1 -1
- package/src/__tests__/scripts/build-integration.test.ts +19 -19
- package/src/__tests__/scripts/dataset-turtle.test.ts +1 -1
- package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +1 -1
- package/src/__tests__/scripts/normalize-yaml.test.ts +1 -1
- package/src/__tests__/scripts/turtle-escape.test.ts +1 -1
- package/src/__tests__/scripts/validate-yaml.test.ts +76 -0
- package/src/__tests__/scripts/version-turtle.test.ts +1 -1
- package/src/__tests__/scripts/vocab-turtle.test.ts +1 -1
- package/src/__tests__/scripts-syntax.test.ts +2 -2
- package/src/__tests__/use-rdf-document.test.ts +2 -3
- package/src/__tests__/utils/content-renderer-xss.test.ts +57 -0
- package/src/__tests__/validate-shacl.test.ts +3 -3
- package/src/adapters/wire/index.ts +69 -0
- package/src/utils/content-renderer.ts +7 -3
- package/src/utils/url-safety.ts +1 -5
- package/tsconfig.json +2 -2
- package/vite.config.ts +1 -1
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +0 -167
- package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +0 -166
- package/src/types/agents-version-turtle.d.ts +0 -27
- package/src/types/bibliography-turtle.d.ts +0 -12
- package/src/types/build-activity-turtle.d.ts +0 -16
- package/src/types/build-cache.d.ts +0 -20
- package/src/types/dataset-turtle.d.ts +0 -32
- package/src/types/normalize-yaml.d.ts +0 -16
- package/src/types/turtle-escape.d.ts +0 -6
- package/src/types/vocab-turtle.d.ts +0 -13
- /package/scripts/{doctor.mjs → doctor.ts} +0 -0
- /package/scripts/{extract-source-refs.js → extract-source-refs.ts} +0 -0
- /package/scripts/{generate-404.js → generate-404.ts} +0 -0
- /package/scripts/{generate-ontology-data.mjs → generate-ontology-data.ts} +0 -0
- /package/scripts/lib/{agents-turtle.mjs → agents-turtle.ts} +0 -0
- /package/scripts/lib/{bibliography-turtle.mjs → bibliography-turtle.ts} +0 -0
- /package/scripts/lib/{bibliography.mjs → bibliography.ts} +0 -0
- /package/scripts/lib/build/{image-assets.mjs → image-assets.ts} +0 -0
- /package/scripts/lib/build/{non-verbal-consumer.mjs → non-verbal-consumer.ts} +0 -0
- /package/scripts/lib/{build-activity-turtle.mjs → build-activity-turtle.ts} +0 -0
- /package/scripts/lib/{concept-groups.mjs → concept-groups.ts} +0 -0
- /package/scripts/lib/{first-non-empty.mjs → first-non-empty.ts} +0 -0
- /package/scripts/lib/{turtle-escape.mjs → turtle-escape.ts} +0 -0
- /package/scripts/lib/{version-turtle.mjs → version-turtle.ts} +0 -0
- /package/scripts/lib/{vocab-turtle.mjs → vocab-turtle.ts} +0 -0
- /package/scripts/{load-site-config.mjs → load-site-config.ts} +0 -0
- /package/scripts/{migrate-v1-to-v3.mjs → migrate-v1-to-v3.ts} +0 -0
- /package/scripts/{normalize-yaml.mjs → normalize-yaml.ts} +0 -0
- /package/scripts/{sync-concept-model.mjs → sync-concept-model.ts} +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concept format emitters — Turtle, SKOS JSON-LD, TBX.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from generate-data.ts. Each function takes a JSON-LD concept
|
|
5
|
+
* object and serializes it to a different interchange format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ttlLit } from './turtle-escape';
|
|
9
|
+
|
|
10
|
+
export function escapeTurtle(s: string): string {
|
|
11
|
+
return ttlLit(s).slice(1, -1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function escapeXml(s: any): string {
|
|
15
|
+
const str = Array.isArray(s) ? s.join(', ') : String(s ?? '');
|
|
16
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function conceptJsonToTurtle(concept: Record<string, any>): string {
|
|
20
|
+
const uri = concept['@id'] || '';
|
|
21
|
+
const id = concept['gl:identifier'] || '';
|
|
22
|
+
const lines = [
|
|
23
|
+
'@prefix skos: <http://www.w3.org/2004/02/skos/core#> .',
|
|
24
|
+
'@prefix dcterms: <http://purl.org/dc/terms/> .',
|
|
25
|
+
'',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const props = [' a skos:Concept'];
|
|
29
|
+
props.push(` skos:notation "${escapeTurtle(id)}"`);
|
|
30
|
+
|
|
31
|
+
for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {}) as [string, any][]) {
|
|
32
|
+
if (lc['gl:designation']) {
|
|
33
|
+
for (const d of lc['gl:designation']) {
|
|
34
|
+
const term = d['gl:term'];
|
|
35
|
+
if (!term) continue;
|
|
36
|
+
const pred = d['gl:normativeStatus'] === 'preferred' ? 'skos:prefLabel' : 'skos:altLabel';
|
|
37
|
+
props.push(` ${pred} "${escapeTurtle(term)}"@${lang}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (lc['gl:definition']) {
|
|
41
|
+
for (const d of lc['gl:definition']) {
|
|
42
|
+
if (d['gl:content']) props.push(` skos:definition "${escapeTurtle(d['gl:content'])}"@${lang}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (lc['gl:notes']) {
|
|
46
|
+
for (const d of lc['gl:notes']) {
|
|
47
|
+
if (d['gl:content']) props.push(` skos:scopeNote "${escapeTurtle(d['gl:content'])}"@${lang}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lines.push(`<${uri}>`);
|
|
53
|
+
lines.push(props.join(' ;\n'));
|
|
54
|
+
lines.push(' .');
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function conceptJsonToSkosJsonLd(concept: Record<string, any>): string {
|
|
59
|
+
const uri = concept['@id'] || '';
|
|
60
|
+
const id = concept['gl:identifier'] || '';
|
|
61
|
+
|
|
62
|
+
const doc: Record<string, any> = {
|
|
63
|
+
'@context': {
|
|
64
|
+
skos: 'http://www.w3.org/2004/02/skos/core#',
|
|
65
|
+
dcterms: 'http://purl.org/dc/terms/',
|
|
66
|
+
'@language': { '@container': '@language' },
|
|
67
|
+
},
|
|
68
|
+
'@id': uri,
|
|
69
|
+
'@type': 'skos:Concept',
|
|
70
|
+
'skos:notation': id,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const prefLabels: Record<string, string> = {};
|
|
74
|
+
const altLabels: Record<string, string> = {};
|
|
75
|
+
const definitions: Record<string, string> = {};
|
|
76
|
+
const scopeNotes: Record<string, string> = {};
|
|
77
|
+
for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {}) as [string, any][]) {
|
|
78
|
+
const descs = lc['gl:designation'] || [];
|
|
79
|
+
const pref = descs.find((d: any) => d['gl:normativeStatus'] === 'preferred' && d['gl:term']);
|
|
80
|
+
const alt = descs.find((d: any) => d['gl:normativeStatus'] !== 'preferred' && d['gl:term']);
|
|
81
|
+
if (pref) prefLabels[lang] = pref['gl:term'];
|
|
82
|
+
if (alt) altLabels[lang] = alt['gl:term'];
|
|
83
|
+
const def = (lc['gl:definition'] || [])[0];
|
|
84
|
+
if (def?.['gl:content']) definitions[lang] = def['gl:content'];
|
|
85
|
+
const note = (lc['gl:notes'] || [])[0];
|
|
86
|
+
if (note?.['gl:content']) scopeNotes[lang] = note['gl:content'];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Object.keys(prefLabels).length) doc['skos:prefLabel'] = prefLabels;
|
|
90
|
+
if (Object.keys(altLabels).length) doc['skos:altLabel'] = altLabels;
|
|
91
|
+
if (Object.keys(definitions).length) doc['skos:definition'] = definitions;
|
|
92
|
+
if (Object.keys(scopeNotes).length) doc['skos:scopeNote'] = scopeNotes;
|
|
93
|
+
|
|
94
|
+
return JSON.stringify(doc);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function conceptJsonToTbx(concept: Record<string, any>): string {
|
|
98
|
+
const id = concept['gl:identifier'] || '';
|
|
99
|
+
const uri = concept['@id'] || '';
|
|
100
|
+
const localized = concept['gl:localizedConcept'] || {};
|
|
101
|
+
|
|
102
|
+
const langSections: { lang: string; termEntries: string[]; blocks: string }[] = [];
|
|
103
|
+
for (const [lang, lc] of Object.entries(localized) as [string, any][]) {
|
|
104
|
+
const descs = lc['gl:designation'] || [];
|
|
105
|
+
const definitions = (lc['gl:definition'] || []).filter((d: any) => d['gl:content']);
|
|
106
|
+
const notes = (lc['gl:notes'] || []).filter((d: any) => d['gl:content']);
|
|
107
|
+
const examples = (lc['gl:examples'] || []).filter((d: any) => d['gl:content']);
|
|
108
|
+
const sources = lc['gl:source'] || [];
|
|
109
|
+
const entryStatus = lc['gl:entryStatus'] || '';
|
|
110
|
+
|
|
111
|
+
if (!descs.length && !definitions.length) continue;
|
|
112
|
+
|
|
113
|
+
const termEntries: string[] = [];
|
|
114
|
+
for (const d of descs) {
|
|
115
|
+
const term = d['gl:term'];
|
|
116
|
+
if (!term) continue;
|
|
117
|
+
const status = d['gl:normativeStatus'] || '';
|
|
118
|
+
const type = d['@type'] || '';
|
|
119
|
+
let gramGrp = '';
|
|
120
|
+
if (d['gl:grammarInfo'] && d['gl:grammarInfo'].length > 0) {
|
|
121
|
+
const gi = d['gl:grammarInfo'][0];
|
|
122
|
+
if (gi['gl:gender']) gramGrp = `\n <grammaticalGender>${escapeXml(gi['gl:gender'])}</grammaticalGender>`;
|
|
123
|
+
if (gi['gl:number']) gramGrp += `\n <grammaticalNumber>${escapeXml(gi['gl:number'])}</grammaticalNumber>`;
|
|
124
|
+
for (const pos of ['noun', 'verb', 'adj', 'adverb', 'preposition', 'participle']) {
|
|
125
|
+
if (gi[`gl:${pos}`]) gramGrp += `\n <partOfSpeech>${pos}</partOfSpeech>`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let posBlock = '';
|
|
129
|
+
if (type.includes('Abbreviation')) posBlock = '\n <partOfSpeech>abbreviation</partOfSpeech>';
|
|
130
|
+
if (type.includes('Symbol')) posBlock = '\n <partOfSpeech>symbol</partOfSpeech>';
|
|
131
|
+
|
|
132
|
+
termEntries.push(` <termEntry>
|
|
133
|
+
<langSet xml:lang="${lang}">
|
|
134
|
+
<tig>
|
|
135
|
+
<term>${escapeXml(term)}</term>${gramGrp}${posBlock}
|
|
136
|
+
</tig>
|
|
137
|
+
</langSet>
|
|
138
|
+
</termEntry>`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let defBlock = '';
|
|
142
|
+
if (definitions.length) {
|
|
143
|
+
const defParts = definitions.map((d: any) => ` <p>${escapeXml(d['gl:content'])}</p>`).join('\n');
|
|
144
|
+
defBlock = `\n <descrip type="definition">\n${defParts}\n </descrip>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let noteBlock = '';
|
|
148
|
+
for (let i = 0; i < notes.length; i++) {
|
|
149
|
+
noteBlock += `\n <note type="note">${escapeXml(notes[i]['gl:content'])}</note>`;
|
|
150
|
+
}
|
|
151
|
+
for (let i = 0; i < examples.length; i++) {
|
|
152
|
+
noteBlock += `\n <note type="example">${escapeXml(examples[i]['gl:content'])}</note>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let sourceBlock = '';
|
|
156
|
+
for (const src of sources) {
|
|
157
|
+
const origin = src['gl:origin'] || {};
|
|
158
|
+
const parts: string[] = [];
|
|
159
|
+
const ref = origin['gl:ref'];
|
|
160
|
+
if (ref) {
|
|
161
|
+
const refParts: string[] = [];
|
|
162
|
+
if (ref['gl:source']) refParts.push(ref['gl:source']);
|
|
163
|
+
if (ref['gl:id']) refParts.push(ref['gl:id']);
|
|
164
|
+
parts.push(refParts.join(' ') || '');
|
|
165
|
+
}
|
|
166
|
+
if (origin['gl:locality']) {
|
|
167
|
+
const loc = origin['gl:locality'];
|
|
168
|
+
if (loc['gl:referenceFrom']) parts.push(loc['gl:localityType'] ? `${loc['gl:localityType']} ${loc['gl:referenceFrom']}` : loc['gl:referenceFrom']);
|
|
169
|
+
}
|
|
170
|
+
if (parts.filter(Boolean).length) {
|
|
171
|
+
sourceBlock += `\n <ref>${escapeXml(parts.filter(Boolean).join(', '))}</ref>`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let statusBlock = '';
|
|
176
|
+
if (entryStatus) {
|
|
177
|
+
statusBlock += `\n <descrip type="entryStatus">${escapeXml(entryStatus)}</descrip>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const termEntriesBlock = termEntries.length ? '\n' + termEntries.join('\n') : '';
|
|
181
|
+
langSections.push({ lang, termEntries, blocks: [defBlock, noteBlock, sourceBlock, statusBlock].filter(b => b).join('') });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!langSections.length) return '';
|
|
185
|
+
|
|
186
|
+
const bodyEntries = langSections.map(ls => {
|
|
187
|
+
return ` <languageSection xml:lang="${ls.lang}">${ls.blocks}\n </languageSection>`;
|
|
188
|
+
}).join('\n');
|
|
189
|
+
|
|
190
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
191
|
+
<tbx style="dca" type="TBX-Basic" xml:lang="en" xmlns="urn:iso:std:iso:30042:ed-2">
|
|
192
|
+
<tbxHeader>
|
|
193
|
+
<fileDesc>
|
|
194
|
+
<source>${escapeXml(uri)}</source>
|
|
195
|
+
</fileDesc>
|
|
196
|
+
</tbxHeader>
|
|
197
|
+
<text>
|
|
198
|
+
<body>
|
|
199
|
+
<conceptEntry id="${escapeXml(id)}">
|
|
200
|
+
${bodyEntries}
|
|
201
|
+
</conceptEntry>
|
|
202
|
+
</body>
|
|
203
|
+
</text>
|
|
204
|
+
</tbx>
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concept URI construction — single source of truth.
|
|
3
|
+
*
|
|
4
|
+
* Previously duplicated in:
|
|
5
|
+
* scripts/generate-data.ts
|
|
6
|
+
* scripts/bridge-to-astro.ts
|
|
7
|
+
* scripts/build-edges.ts
|
|
8
|
+
*
|
|
9
|
+
* DRY: all three now import from here.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function buildConceptUri(uriBase: string, registerId: string, conceptId: string | number): string {
|
|
13
|
+
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
14
|
+
}
|
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
writeTurtle,
|
|
12
12
|
PREFIXES,
|
|
13
13
|
} from 'glossarist/rdf';
|
|
14
|
+
import type { PrefixMap } from './yaml-types';
|
|
14
15
|
|
|
15
|
-
function writerPrefixes() {
|
|
16
|
-
const out = {};
|
|
16
|
+
function writerPrefixes(): PrefixMap {
|
|
17
|
+
const out: PrefixMap = {};
|
|
17
18
|
// Only the prefixes this emitter actually uses — keeps the output
|
|
18
19
|
// header focused.
|
|
19
20
|
for (const k of ['dcat', 'skos', 'dcterms', 'prov', 'rdf', 'rdfs', 'xsd']) {
|
|
@@ -27,7 +28,7 @@ function writerPrefixes() {
|
|
|
27
28
|
return out;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export async function buildDatasetTurtle(input) {
|
|
31
|
+
export async function buildDatasetTurtle(input: any): Promise<string> {
|
|
31
32
|
const quads = collectQuads(datasetToQuads(input));
|
|
32
33
|
return writeTurtle(quads, { prefixes: writerPrefixes() });
|
|
33
34
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML concept harmonization — normalizes multi-doc and simple formats
|
|
3
|
+
* into a single HarmonizedConcept shape.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from generate-data.ts for testability and reuse.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import yaml from 'js-yaml';
|
|
10
|
+
import type { HarmonizedConcept, YamlManagedConceptDoc } from './yaml-types';
|
|
11
|
+
|
|
12
|
+
export const LEGACY_MULTIPLICITY_MAP: Record<string, { presence: string; count: string }> = {
|
|
13
|
+
compulsory: { presence: 'required', count: 'exactly_one' },
|
|
14
|
+
optional: { presence: 'optional', count: 'exactly_one' },
|
|
15
|
+
compulsory_multiple: { presence: 'required', count: 'multiple' },
|
|
16
|
+
optional_multiple: { presence: 'optional', count: 'multiple' },
|
|
17
|
+
compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const LEGACY_CERTAINTY_FROM_AXIS: Record<string, string> = {
|
|
21
|
+
required: 'confirmed',
|
|
22
|
+
optional: 'possible',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function splitMultiplicity(multiplicity: string): { presence: string; count: string } {
|
|
26
|
+
return multiplicity in LEGACY_MULTIPLICITY_MAP
|
|
27
|
+
? LEGACY_MULTIPLICITY_MAP[multiplicity]
|
|
28
|
+
: { presence: 'required', count: 'exactly_one' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadConceptFile(filePath: string): HarmonizedConcept {
|
|
32
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
33
|
+
const docs = yaml.loadAll(content, null, { schema: yaml.DEFAULT_SCHEMA }) as Record<string, any>[];
|
|
34
|
+
|
|
35
|
+
if (docs.length === 1 && docs[0].termid !== undefined) {
|
|
36
|
+
return docs[0] as HarmonizedConcept;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (docs.length >= 1 && docs[0].data && docs[0].data.identifier !== undefined) {
|
|
40
|
+
const mc = docs[0] as YamlManagedConceptDoc;
|
|
41
|
+
const result: HarmonizedConcept = { termid: String(mc.data!.identifier) };
|
|
42
|
+
|
|
43
|
+
if (mc.related) result._related = mc.related as any;
|
|
44
|
+
if (mc.partitive_relations) result._partitiveRelations = mc.partitive_relations as any;
|
|
45
|
+
if (mc.generic_relations) result._genericRelations = mc.generic_relations as any;
|
|
46
|
+
if (mc.data!.domains) result._domains = mc.data!.domains;
|
|
47
|
+
if (mc.dates) result._dates = mc.dates as any;
|
|
48
|
+
if (mc.sources) result._sources = mc.sources as any;
|
|
49
|
+
if (mc.status) result._status = mc.status;
|
|
50
|
+
if (mc.schema_version) result._schemaVersion = mc.schema_version;
|
|
51
|
+
if (mc.date_accepted) result._dateAccepted = mc.date_accepted;
|
|
52
|
+
|
|
53
|
+
const MERGE_KEYS = [
|
|
54
|
+
'terms', 'definition', 'notes', 'annotations', 'examples', 'sources',
|
|
55
|
+
'dates', 'domain', 'references', 'entry_status', 'classification',
|
|
56
|
+
'review_type', 'review_date', 'review_decision_date', 'review_decision_event',
|
|
57
|
+
'review_status', 'review_decision', 'review_decision_notes',
|
|
58
|
+
'lineage_source_similarity', 'release', 'script', 'system',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const doc of docs.slice(1)) {
|
|
62
|
+
if (!doc) continue;
|
|
63
|
+
const lang = doc.data?.language_code || doc.language_code;
|
|
64
|
+
if (!lang) continue;
|
|
65
|
+
const lcData = { ...(doc.data || {}) };
|
|
66
|
+
delete lcData.language_code;
|
|
67
|
+
for (const key of MERGE_KEYS) {
|
|
68
|
+
if (doc[key] !== undefined && lcData[key] === undefined) {
|
|
69
|
+
lcData[key] = doc[key];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
(result as Record<string, any>)[lang] = lcData;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return docs[0] as HarmonizedConcept;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function readYaml(filePath: string): any {
|
|
81
|
+
return yaml.load(fs.readFileSync(filePath, 'utf8'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function stripHtml(s: string): string {
|
|
85
|
+
return s.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
86
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-LD wire-format emitters — convert YAML author objects to gl:* JSON-LD.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from generate-data.ts. Each function maps one YAML entity
|
|
5
|
+
* (term, source, date, relation) to its JSON-LD wire representation.
|
|
6
|
+
*
|
|
7
|
+
* The 'gl:' prefix is the Glossarist JSON-LD namespace — see
|
|
8
|
+
* data/concept-model/ontologies/glossarist.context.jsonld.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function resolveRefUri(term: string, refMaps: any): string | undefined {
|
|
12
|
+
if (!refMaps) return undefined;
|
|
13
|
+
const lc = term.toLowerCase();
|
|
14
|
+
if (refMaps.urnMap?.[lc]) return refMaps.urnMap[lc];
|
|
15
|
+
if (refMaps.designationMap?.[lc]) return refMaps.designationMap[lc];
|
|
16
|
+
if (refMaps.numericMap?.[term]) return refMaps.numericMap[term];
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function termToDesignation(term: Record<string, any>): Record<string, any> {
|
|
21
|
+
const typeMap: Record<string, string> = {
|
|
22
|
+
expression: 'gl:Expression',
|
|
23
|
+
abbreviation: 'gl:Abbreviation',
|
|
24
|
+
symbol: 'gl:Symbol',
|
|
25
|
+
letter_symbol: 'gl:LetterSymbol',
|
|
26
|
+
'graphical symbol': 'gl:GraphicalSymbol',
|
|
27
|
+
};
|
|
28
|
+
const doc: Record<string, any> = {
|
|
29
|
+
'@type': typeMap[term.type] || 'gl:Designation',
|
|
30
|
+
'gl:normativeStatus': term.normative_status || 'preferred',
|
|
31
|
+
'gl:term': term.designation,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (term.grammar_info && term.grammar_info.length > 0) {
|
|
35
|
+
doc['gl:grammarInfo'] = term.grammar_info.map((gi: Record<string, any>) => {
|
|
36
|
+
const g: Record<string, any> = {};
|
|
37
|
+
if (gi.gender) g['gl:gender'] = gi.gender;
|
|
38
|
+
if (gi.number) g['gl:number'] = gi.number;
|
|
39
|
+
if (gi.partOfSpeech) g['gl:partOfSpeech'] = gi.partOfSpeech;
|
|
40
|
+
for (const pos of ['noun', 'verb', 'adj', 'adverb', 'preposition', 'participle']) {
|
|
41
|
+
if (gi[pos]) g[`gl:${pos}`] = gi[pos];
|
|
42
|
+
}
|
|
43
|
+
return g;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (term.international !== undefined) doc['gl:international'] = term.international;
|
|
48
|
+
if (term.absent !== undefined) doc['gl:absent'] = term.absent;
|
|
49
|
+
if (term.geographical_area) doc['gl:geographicalArea'] = term.geographical_area;
|
|
50
|
+
if (term.term_type) doc['gl:termType'] = term.term_type;
|
|
51
|
+
if (term.prefix) doc['gl:prefix'] = term.prefix;
|
|
52
|
+
if (term.usage_info) doc['gl:usageInfo'] = term.usage_info;
|
|
53
|
+
if (term.field_of_application) doc['gl:fieldOfApplication'] = term.field_of_application;
|
|
54
|
+
if (term.acronym !== undefined) doc['gl:acronym'] = term.acronym;
|
|
55
|
+
if (term.initialism !== undefined) doc['gl:initialism'] = term.initialism;
|
|
56
|
+
if (term.truncation !== undefined) doc['gl:truncation'] = term.truncation;
|
|
57
|
+
if (term.text) doc['gl:text'] = term.text;
|
|
58
|
+
if (term.image) doc['gl:image'] = term.image;
|
|
59
|
+
|
|
60
|
+
if (term.related && term.related.length > 0) {
|
|
61
|
+
doc['gl:related'] = term.related.map((r: Record<string, any>) => {
|
|
62
|
+
const rel: Record<string, any> = {};
|
|
63
|
+
if (r.type) rel['gl:relationshipType'] = r.type;
|
|
64
|
+
if (r.target) {
|
|
65
|
+
rel['gl:target'] = r.target;
|
|
66
|
+
} else if (r.ref) {
|
|
67
|
+
const ref: Record<string, any> = { '@type': 'gl:ConceptRef' };
|
|
68
|
+
if (r.ref.source) ref['gl:source'] = r.ref.source;
|
|
69
|
+
if (r.ref.id) ref['gl:id'] = r.ref.id;
|
|
70
|
+
if (r.ref.text) ref['gl:text'] = r.ref.text;
|
|
71
|
+
rel['gl:ref'] = ref;
|
|
72
|
+
}
|
|
73
|
+
return rel;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return doc;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function defsToJsonLd(defs: any[]): Record<string, any>[] {
|
|
81
|
+
if (!defs || !Array.isArray(defs)) return [];
|
|
82
|
+
return defs
|
|
83
|
+
.map(d => ({ '@type': 'gl:DetailedDefinition', 'gl:content': d.content || '' }))
|
|
84
|
+
.filter(d => d['gl:content']);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function refToJsonLd(ref: any, typeName = 'gl:Ref'): Record<string, any> | undefined {
|
|
88
|
+
if (!ref) return undefined;
|
|
89
|
+
const refObj: Record<string, any> = { '@type': typeName };
|
|
90
|
+
if (typeof ref === 'string') {
|
|
91
|
+
refObj['gl:source'] = ref;
|
|
92
|
+
} else {
|
|
93
|
+
if (ref.source) refObj['gl:source'] = ref.source;
|
|
94
|
+
if (ref.id) refObj['gl:id'] = ref.id;
|
|
95
|
+
if (ref.version) refObj['gl:version'] = ref.version;
|
|
96
|
+
if (ref.text) refObj['gl:text'] = ref.text;
|
|
97
|
+
}
|
|
98
|
+
return refObj;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function localityToJsonLd(loc: any): Record<string, any> | undefined {
|
|
102
|
+
if (!loc) return undefined;
|
|
103
|
+
const locObj: Record<string, any> = {};
|
|
104
|
+
if (loc.type) locObj['gl:localityType'] = loc.type;
|
|
105
|
+
if (loc.reference_from) locObj['gl:referenceFrom'] = loc.reference_from;
|
|
106
|
+
if (loc.referenceFrom) locObj['gl:referenceFrom'] = loc.referenceFrom;
|
|
107
|
+
if (loc.reference_to) locObj['gl:referenceTo'] = loc.reference_to;
|
|
108
|
+
if (loc.referenceTo) locObj['gl:referenceTo'] = loc.referenceTo;
|
|
109
|
+
return Object.keys(locObj).length > 0 ? locObj : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function citationToJsonLd(citation: any): Record<string, any> {
|
|
113
|
+
const obj: Record<string, any> = {};
|
|
114
|
+
const ref = refToJsonLd(citation.ref);
|
|
115
|
+
if (ref) obj['gl:ref'] = ref;
|
|
116
|
+
const loc = localityToJsonLd(citation.locality);
|
|
117
|
+
if (loc) obj['gl:locality'] = loc;
|
|
118
|
+
if (citation.link) obj['gl:link'] = citation.link;
|
|
119
|
+
return obj;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function sourcesToJsonLd(sources: any[]): Record<string, any>[] {
|
|
123
|
+
if (!sources || !Array.isArray(sources)) return [];
|
|
124
|
+
return sources.map(s => {
|
|
125
|
+
const doc: Record<string, any> = { '@type': 'gl:ConceptSource' };
|
|
126
|
+
if (s.id) doc['gl:id'] = s.id;
|
|
127
|
+
if (s.type) doc['gl:sourceType'] = s.type;
|
|
128
|
+
if (s.status) doc['gl:sourceStatus'] = s.status;
|
|
129
|
+
if (s.modification) doc['gl:modification'] = s.modification;
|
|
130
|
+
if (s.origin) {
|
|
131
|
+
const origin: Record<string, any> = { '@type': 'gl:Citation' };
|
|
132
|
+
const ref = refToJsonLd(s.origin.ref);
|
|
133
|
+
if (ref) origin['gl:ref'] = ref;
|
|
134
|
+
const loc = localityToJsonLd(s.origin.locality);
|
|
135
|
+
if (loc) origin['gl:locality'] = loc;
|
|
136
|
+
if (s.origin.link) origin['gl:link'] = s.origin.link;
|
|
137
|
+
doc['gl:origin'] = origin;
|
|
138
|
+
}
|
|
139
|
+
if (s.sourced_from && s.sourced_from.length) {
|
|
140
|
+
doc['gl:sourcedFrom'] = s.sourced_from.map((sf: any) => {
|
|
141
|
+
const cite: Record<string, any> = { '@type': 'gl:Citation' };
|
|
142
|
+
const ref = refToJsonLd(sf.ref);
|
|
143
|
+
if (ref) cite['gl:ref'] = ref;
|
|
144
|
+
const loc = localityToJsonLd(sf.locality);
|
|
145
|
+
if (loc) cite['gl:locality'] = loc;
|
|
146
|
+
if (sf.link) cite['gl:link'] = sf.link;
|
|
147
|
+
return cite;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return doc;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function refsToJsonLd(refs: any[], refMaps: any): Record<string, any>[] {
|
|
155
|
+
if (!refs || !Array.isArray(refs)) return [];
|
|
156
|
+
return refs.map(r => {
|
|
157
|
+
if (r.id) {
|
|
158
|
+
const ref: Record<string, any> = { '@id': r.id, 'gl:term': r.term };
|
|
159
|
+
if (r.sourceId) ref['gl:sourceId'] = r.sourceId;
|
|
160
|
+
if (r.citation) ref['gl:citation'] = citationToJsonLd(r.citation);
|
|
161
|
+
return ref;
|
|
162
|
+
}
|
|
163
|
+
if (r.term && refMaps) {
|
|
164
|
+
const uri = resolveRefUri(r.term, refMaps);
|
|
165
|
+
if (uri) return { '@id': uri, 'gl:term': r.term };
|
|
166
|
+
}
|
|
167
|
+
return { '@id': r.id || r.term, 'gl:term': r.term };
|
|
168
|
+
}).filter(r => r['@id']);
|
|
169
|
+
}
|
|
@@ -24,14 +24,18 @@ import path from 'path';
|
|
|
24
24
|
* (`/var/folders/...`) is a symlink to `/private/var/folders/...`; without
|
|
25
25
|
* this, prefix comparisons across the symlink boundary silently fail.
|
|
26
26
|
*/
|
|
27
|
-
function physicalPath(p) {
|
|
27
|
+
function physicalPath(p: string): string {
|
|
28
28
|
if (fs.existsSync(p)) return fs.realpathSync(p);
|
|
29
29
|
const parent = path.dirname(p);
|
|
30
30
|
const parentReal = fs.existsSync(parent) ? fs.realpathSync(parent) : physicalPath(parent);
|
|
31
31
|
return path.join(parentReal, path.basename(p));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function assertLocalPathSafe(
|
|
34
|
+
export function assertLocalPathSafe(
|
|
35
|
+
datasetId: string,
|
|
36
|
+
localPath: string,
|
|
37
|
+
{ root = process.cwd(), datasetsDir }: { root?: string; datasetsDir?: string } = {},
|
|
38
|
+
): string {
|
|
35
39
|
const datasetsRoot = datasetsDir || path.join(root, '.datasets');
|
|
36
40
|
const localResolved = path.resolve(root, localPath);
|
|
37
41
|
|