@glossarist/concept-browser 0.7.115 → 0.7.116
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/cli/index.mjs +12 -15
- package/package.json +5 -1
- package/scripts/bridge-to-astro.js +132 -0
- package/scripts/build-edges.js +480 -0
- package/scripts/build-scripts.mjs +22 -0
- package/scripts/check-syntax.js +81 -0
- package/scripts/doctor.js +278 -0
- package/scripts/extract-source-refs.js +24 -0
- package/scripts/fetch-datasets.js +278 -0
- package/scripts/generate-404.js +14 -0
- package/scripts/generate-data.js +2220 -0
- package/scripts/generate-ontology-data.js +139 -0
- package/scripts/generate-ontology-schema.js +472 -0
- package/scripts/load-site-config.js +83 -0
- package/scripts/migrate-v1-to-v3.js +113 -0
- package/scripts/normalize-yaml.js +96 -0
- package/scripts/process-about-pages.js +241 -0
- package/scripts/smoke.js +211 -0
- package/scripts/sync-concept-model.js +63 -0
- package/scripts/validate-shacl.js +152 -0
- package/scripts/validate-yaml.js +132 -0
- package/src/__tests__/published-package-contract.test.ts +84 -0
- package/src/__tests__/scripts-syntax.test.ts +2 -2
- package/src/data/ontology-schema.json +1336 -183
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/generate-ontology-data.ts
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
5
|
+
import { resolve, dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
var ROOT = resolve(__dirname, "..");
|
|
9
|
+
var TAXONOMY_DIR = resolve(ROOT, "data", "concept-model", "taxonomies");
|
|
10
|
+
var OUTPUT = resolve(ROOT, "src", "data", "taxonomies.json");
|
|
11
|
+
function parseTurtle(text) {
|
|
12
|
+
const cleaned = text.replace(/#[^\n]*/g, "");
|
|
13
|
+
const subjectBlocks = splitSubjects(cleaned);
|
|
14
|
+
const concepts = {};
|
|
15
|
+
let scheme = null;
|
|
16
|
+
let schemeLabel = null;
|
|
17
|
+
let schemeDefinition = null;
|
|
18
|
+
for (const block of subjectBlocks) {
|
|
19
|
+
const trimmed = block.trim();
|
|
20
|
+
if (!trimmed) continue;
|
|
21
|
+
const subjectMatch = trimmed.match(/^([^\s]+)\s+a\s+(.+?)(?:\s*[;.,]|$)/s);
|
|
22
|
+
if (!subjectMatch) continue;
|
|
23
|
+
const subject = subjectMatch[1];
|
|
24
|
+
const types = subjectMatch[2];
|
|
25
|
+
const isScheme = /\bskos:ConceptScheme\b/.test(types);
|
|
26
|
+
const isConcept = /\bskos:Concept\b/.test(types);
|
|
27
|
+
if (isScheme) {
|
|
28
|
+
scheme = subject;
|
|
29
|
+
schemeLabel = findLiteral(trimmed, "skos:prefLabel");
|
|
30
|
+
schemeDefinition = findLiteral(trimmed, "skos:definition");
|
|
31
|
+
}
|
|
32
|
+
if (isConcept && !isScheme) {
|
|
33
|
+
const id = subject.includes("/") ? subject.split("/").pop() : subject;
|
|
34
|
+
const prefLabel = findLiteral(trimmed, "skos:prefLabel");
|
|
35
|
+
const altLabel = findLiteral(trimmed, "skos:altLabel");
|
|
36
|
+
const definition = findLiteral(trimmed, "skos:definition");
|
|
37
|
+
const broader = findResource(trimmed, "skos:broader");
|
|
38
|
+
concepts[id] = {
|
|
39
|
+
id,
|
|
40
|
+
iri: subject,
|
|
41
|
+
prefLabel: prefLabel || id,
|
|
42
|
+
...altLabel && { altLabel },
|
|
43
|
+
...definition && { definition },
|
|
44
|
+
...broader && { broader }
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { scheme, schemeLabel, schemeDefinition, concepts };
|
|
49
|
+
}
|
|
50
|
+
function splitSubjects(text) {
|
|
51
|
+
const blocks = [];
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let start = -1;
|
|
54
|
+
for (let i = 0; i < text.length; i++) {
|
|
55
|
+
const ch = text[i];
|
|
56
|
+
if (ch === '"') {
|
|
57
|
+
i++;
|
|
58
|
+
while (i < text.length && text[i] !== '"') {
|
|
59
|
+
if (text[i] === "\\") i++;
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === "[") depth++;
|
|
65
|
+
if (ch === "]") depth--;
|
|
66
|
+
if (ch === "(") depth++;
|
|
67
|
+
if (ch === ")") depth--;
|
|
68
|
+
if (depth === 0 && ch === "." && start >= 0) {
|
|
69
|
+
blocks.push(text.slice(start, i));
|
|
70
|
+
start = -1;
|
|
71
|
+
} else if (start < 0 && ch === "\n") {
|
|
72
|
+
const rest = text.slice(i).trimStart();
|
|
73
|
+
if (rest && !rest.startsWith("#")) {
|
|
74
|
+
start = i + 1;
|
|
75
|
+
}
|
|
76
|
+
} else if (start < 0 && /[^\s]/.test(ch)) {
|
|
77
|
+
start = i;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return blocks;
|
|
81
|
+
}
|
|
82
|
+
function findLiteral(block, predicate) {
|
|
83
|
+
const tripleQuoted = new RegExp(predicate + '\\s+"""([^"]*?)"""@en');
|
|
84
|
+
let m = block.match(tripleQuoted);
|
|
85
|
+
if (m) return m[1].replace(/\s+/g, " ").trim();
|
|
86
|
+
const singleQuoted = new RegExp(predicate + '\\s+"([^"]*?)"@en');
|
|
87
|
+
m = block.match(singleQuoted);
|
|
88
|
+
return m ? m[1].replace(/\s+/g, " ").trim() : null;
|
|
89
|
+
}
|
|
90
|
+
function findResource(block, predicate) {
|
|
91
|
+
const re = new RegExp(predicate + "\\s+([^\\s,;]+)");
|
|
92
|
+
const m = block.match(re);
|
|
93
|
+
if (!m) return null;
|
|
94
|
+
const val = m[1].replace(/[;.]+$/, "");
|
|
95
|
+
return val.includes("/") ? val.split("/").pop() : val;
|
|
96
|
+
}
|
|
97
|
+
var TAXONOMY_MAP = {
|
|
98
|
+
"concept-status.ttl": "conceptStatus",
|
|
99
|
+
"entry-status.ttl": "entryStatus",
|
|
100
|
+
"normative-status.ttl": "normativeStatus",
|
|
101
|
+
"source-type.ttl": "sourceType",
|
|
102
|
+
"source-status.ttl": "sourceStatus",
|
|
103
|
+
"relationship-type.ttl": "relationshipType",
|
|
104
|
+
"designation-type.ttl": "designationType",
|
|
105
|
+
"term-type.ttl": "termType",
|
|
106
|
+
"grammar-gender.ttl": "grammarGender",
|
|
107
|
+
"grammar-number.ttl": "grammarNumber"
|
|
108
|
+
};
|
|
109
|
+
function main() {
|
|
110
|
+
if (!existsSync(TAXONOMY_DIR)) {
|
|
111
|
+
console.error(`Taxonomy directory not found: ${TAXONOMY_DIR}`);
|
|
112
|
+
console.error("Run `npm run sync:model` to vendor concept-model data.");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const result = {};
|
|
116
|
+
for (const [filename, key] of Object.entries(TAXONOMY_MAP)) {
|
|
117
|
+
const filepath = join(TAXONOMY_DIR, filename);
|
|
118
|
+
if (!existsSync(filepath)) {
|
|
119
|
+
console.warn(` Skipping ${filename} (not found)`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const text = readFileSync(filepath, "utf-8");
|
|
123
|
+
const parsed = parseTurtle(text);
|
|
124
|
+
result[key] = {
|
|
125
|
+
scheme: parsed.scheme,
|
|
126
|
+
schemeLabel: parsed.schemeLabel,
|
|
127
|
+
schemeDefinition: parsed.schemeDefinition,
|
|
128
|
+
concepts: parsed.concepts
|
|
129
|
+
};
|
|
130
|
+
const count = Object.keys(parsed.concepts).length;
|
|
131
|
+
console.log(` ${key}: ${count} concepts`);
|
|
132
|
+
}
|
|
133
|
+
mkdirSync(dirname(OUTPUT), { recursive: true });
|
|
134
|
+
writeFileSync(OUTPUT, JSON.stringify(result, null, 2) + "\n");
|
|
135
|
+
console.log(`
|
|
136
|
+
Wrote ${OUTPUT}`);
|
|
137
|
+
console.log(`Total taxonomies: ${Object.keys(result).length}`);
|
|
138
|
+
}
|
|
139
|
+
main();
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/generate-ontology-schema.ts
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
5
|
+
import { resolve, dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
var ROOT = resolve(__dirname, "..");
|
|
9
|
+
var CONCEPT_MODEL = resolve(ROOT, "data", "concept-model");
|
|
10
|
+
var ONTOLOGY_TTL = resolve(CONCEPT_MODEL, "glossarist.ttl");
|
|
11
|
+
var SHACL_TTL = resolve(CONCEPT_MODEL, "shapes", "glossarist.shacl.ttl");
|
|
12
|
+
var OUTPUT = resolve(ROOT, "src", "data", "ontology-schema.json");
|
|
13
|
+
var KNOWN_PREFIXES = {
|
|
14
|
+
gloss: "https://www.glossarist.org/ontologies/",
|
|
15
|
+
owl: "http://www.w3.org/2002/07/owl#",
|
|
16
|
+
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
17
|
+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
|
|
18
|
+
skos: "http://www.w3.org/2004/02/skos/core#",
|
|
19
|
+
xl: "http://www.w3.org/2008/05/skos-xl#",
|
|
20
|
+
"iso-thes": "http://purl.org/iso25964/skos-thes#",
|
|
21
|
+
dcterms: "http://purl.org/dc/terms/",
|
|
22
|
+
prov: "http://www.w3.org/ns/prov#",
|
|
23
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
24
|
+
sh: "http://www.w3.org/ns/shacl#",
|
|
25
|
+
vann: "http://purl.org/vocab/vann/"
|
|
26
|
+
};
|
|
27
|
+
function expandPrefixed(term) {
|
|
28
|
+
for (const [prefix, uri] of Object.entries(KNOWN_PREFIXES)) {
|
|
29
|
+
if (term.startsWith(prefix + ":")) {
|
|
30
|
+
return uri + term.slice(prefix.length + 1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return term;
|
|
34
|
+
}
|
|
35
|
+
function compactIri(iri) {
|
|
36
|
+
for (const [prefix, uri] of Object.entries(KNOWN_PREFIXES)) {
|
|
37
|
+
if (iri.startsWith(uri)) {
|
|
38
|
+
return prefix + ":" + iri.slice(uri.length);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return iri;
|
|
42
|
+
}
|
|
43
|
+
function splitSubjectBlocks(text) {
|
|
44
|
+
const blocks = [];
|
|
45
|
+
let depth = 0;
|
|
46
|
+
let start = -1;
|
|
47
|
+
let inTripleQuote = false;
|
|
48
|
+
let inUri = false;
|
|
49
|
+
for (let i = 0; i < text.length; i++) {
|
|
50
|
+
const ch = text[i];
|
|
51
|
+
if (inTripleQuote) {
|
|
52
|
+
if (ch === '"' && text.slice(i, i + 3) === '"""') {
|
|
53
|
+
inTripleQuote = false;
|
|
54
|
+
i += 2;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (ch === '"' && text.slice(i, i + 3) === '"""') {
|
|
59
|
+
inTripleQuote = true;
|
|
60
|
+
i += 2;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === '"') {
|
|
64
|
+
i++;
|
|
65
|
+
while (i < text.length && text[i] !== '"') {
|
|
66
|
+
if (text[i] === "\\") i++;
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (inUri) {
|
|
72
|
+
if (ch === ">") inUri = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === "<") {
|
|
76
|
+
inUri = true;
|
|
77
|
+
if (start < 0) start = i;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ch === "[" || ch === "(") depth++;
|
|
81
|
+
if (ch === "]" || ch === ")") depth--;
|
|
82
|
+
if (!inUri && depth === 0 && ch === ".") {
|
|
83
|
+
if (start >= 0) {
|
|
84
|
+
blocks.push(text.slice(start, i));
|
|
85
|
+
start = -1;
|
|
86
|
+
}
|
|
87
|
+
} else if (start < 0 && /\S/.test(ch)) {
|
|
88
|
+
start = i;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return blocks;
|
|
92
|
+
}
|
|
93
|
+
function extractLiteral(block, predicate) {
|
|
94
|
+
const tripleQuoted = new RegExp(predicate + '\\s+"""([^]*?)"""@en');
|
|
95
|
+
let m = block.match(tripleQuoted);
|
|
96
|
+
if (m) return m[1].replace(/\s+/g, " ").trim();
|
|
97
|
+
const singleQuoted = new RegExp(predicate + '\\s+"([^"]*?)"@en');
|
|
98
|
+
m = block.match(singleQuoted);
|
|
99
|
+
if (m) return m[1];
|
|
100
|
+
const plain = new RegExp(predicate + '\\s+"""([^]*?)"""');
|
|
101
|
+
m = block.match(plain);
|
|
102
|
+
if (m) return m[1].replace(/\s+/g, " ").trim();
|
|
103
|
+
const plainSingle = new RegExp(predicate + '\\s+"([^"]*?)"');
|
|
104
|
+
m = block.match(plainSingle);
|
|
105
|
+
return m ? m[1] : null;
|
|
106
|
+
}
|
|
107
|
+
function extractResource(block, predicate) {
|
|
108
|
+
const re = new RegExp(predicate + "\\s+([^\\s,;]+)");
|
|
109
|
+
const m = block.match(re);
|
|
110
|
+
if (!m) return null;
|
|
111
|
+
let val = m[1].replace(/[;.]+$/, "");
|
|
112
|
+
if (val === "a") return null;
|
|
113
|
+
return val;
|
|
114
|
+
}
|
|
115
|
+
function preprocessTtl(ttlText) {
|
|
116
|
+
const lines = ttlText.split("\n");
|
|
117
|
+
const filtered = lines.filter((l) => !l.trimStart().startsWith("@prefix") && !l.trimStart().startsWith("@base"));
|
|
118
|
+
const text = filtered.join("\n");
|
|
119
|
+
let result = "";
|
|
120
|
+
let inTriple = false;
|
|
121
|
+
let inSingle = false;
|
|
122
|
+
let inUri = false;
|
|
123
|
+
for (let i = 0; i < text.length; i++) {
|
|
124
|
+
const ch = text[i];
|
|
125
|
+
if (inTriple) {
|
|
126
|
+
result += ch;
|
|
127
|
+
if (ch === '"' && text.slice(i, i + 3) === '"""') {
|
|
128
|
+
result += text.slice(i + 1, i + 3);
|
|
129
|
+
i += 2;
|
|
130
|
+
inTriple = false;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (inSingle) {
|
|
135
|
+
result += ch;
|
|
136
|
+
if (ch === "\\") {
|
|
137
|
+
result += text[i + 1];
|
|
138
|
+
i++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (ch === '"') inSingle = false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (inUri) {
|
|
145
|
+
result += ch;
|
|
146
|
+
if (ch === ">") inUri = false;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (ch === '"' && text.slice(i, i + 3) === '"""') {
|
|
150
|
+
result += '"""';
|
|
151
|
+
inTriple = true;
|
|
152
|
+
i += 2;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (ch === '"') {
|
|
156
|
+
result += ch;
|
|
157
|
+
inSingle = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === "<") {
|
|
161
|
+
result += ch;
|
|
162
|
+
inUri = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (ch === "#") {
|
|
166
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
167
|
+
if (i < text.length) result += "\n";
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
result += ch;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
function parseOntology(ttlText) {
|
|
175
|
+
const cleaned = preprocessTtl(ttlText);
|
|
176
|
+
const blocks = splitSubjectBlocks(cleaned);
|
|
177
|
+
const classes = [];
|
|
178
|
+
const properties = [];
|
|
179
|
+
for (const block of blocks) {
|
|
180
|
+
const trimmed = block.trim();
|
|
181
|
+
if (!trimmed) continue;
|
|
182
|
+
const subjectMatch = trimmed.match(/^([^\s]+)/);
|
|
183
|
+
if (!subjectMatch) continue;
|
|
184
|
+
const subject = subjectMatch[1];
|
|
185
|
+
if (subject.includes("glossarist>") && !subject.startsWith("gloss:")) continue;
|
|
186
|
+
const typeMatch = trimmed.match(/\ba\s+(.+?)(?:\s*[;.\n]|$)/);
|
|
187
|
+
if (!typeMatch) continue;
|
|
188
|
+
const typeStr = typeMatch[1];
|
|
189
|
+
const isClass = /\bowl:Class\b/.test(typeStr);
|
|
190
|
+
const isObjectProperty = /\bowl:ObjectProperty\b/.test(typeStr);
|
|
191
|
+
const isDatatypeProperty = /\bowl:DatatypeProperty\b/.test(typeStr);
|
|
192
|
+
if (!isClass && !isObjectProperty && !isDatatypeProperty) continue;
|
|
193
|
+
const label = extractLiteral(trimmed, "rdfs:label");
|
|
194
|
+
const comment = extractLiteral(trimmed, "rdfs:comment");
|
|
195
|
+
const iri = expandPrefixed(subject);
|
|
196
|
+
const compact = compactIri(iri);
|
|
197
|
+
if (isClass) {
|
|
198
|
+
const subClassOf = extractResource(trimmed, "rdfs:subClassOf");
|
|
199
|
+
const disjointWith = extractResource(trimmed, "owl:disjointWith");
|
|
200
|
+
classes.push({
|
|
201
|
+
iri,
|
|
202
|
+
compact,
|
|
203
|
+
label: label || subject.replace("gloss:", ""),
|
|
204
|
+
comment,
|
|
205
|
+
subClassOf: subClassOf ? compactIri(expandPrefixed(subClassOf)) : null,
|
|
206
|
+
disjointWith: disjointWith ? compactIri(expandPrefixed(disjointWith)) : null
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
const domain = extractResource(trimmed, "rdfs:domain");
|
|
210
|
+
const range = extractResource(trimmed, "rdfs:range");
|
|
211
|
+
const inverseOf = extractResource(trimmed, "owl:inverseOf");
|
|
212
|
+
let domainUnion = null;
|
|
213
|
+
let rangeUnion = null;
|
|
214
|
+
const unionDomainMatch = trimmed.match(/rdfs:domain\s+\[\s*a\s+owl:Class\s*;\s*owl:unionOf\s*\(([^)]+)\)\s*\]/);
|
|
215
|
+
if (unionDomainMatch) {
|
|
216
|
+
domainUnion = unionDomainMatch[1].trim().split(/\s+/).map((t) => compactIri(expandPrefixed(t)));
|
|
217
|
+
}
|
|
218
|
+
const unionRangeMatch = trimmed.match(/rdfs:range\s+\[\s*a\s+owl:Class\s*;\s*owl:unionOf\s*\(([^)]+)\)\s*\]/);
|
|
219
|
+
if (unionRangeMatch) {
|
|
220
|
+
rangeUnion = unionRangeMatch[1].trim().split(/\s+/).map((t) => compactIri(expandPrefixed(t)));
|
|
221
|
+
}
|
|
222
|
+
properties.push({
|
|
223
|
+
iri,
|
|
224
|
+
compact,
|
|
225
|
+
label: label || subject.replace("gloss:", ""),
|
|
226
|
+
comment,
|
|
227
|
+
type: isObjectProperty ? "object" : "datatype",
|
|
228
|
+
domain: domain ? compactIri(expandPrefixed(domain)) : null,
|
|
229
|
+
domainUnion,
|
|
230
|
+
range: range ? compactIri(expandPrefixed(range)) : null,
|
|
231
|
+
rangeUnion,
|
|
232
|
+
inverseOf: inverseOf ? compactIri(expandPrefixed(inverseOf)) : null
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { classes, properties };
|
|
237
|
+
}
|
|
238
|
+
function buildClassHierarchy(classes) {
|
|
239
|
+
const map = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const c of classes) {
|
|
241
|
+
map.set(c.compact, c);
|
|
242
|
+
c.children = [];
|
|
243
|
+
c.ancestors = [];
|
|
244
|
+
}
|
|
245
|
+
for (const c of classes) {
|
|
246
|
+
if (c.subClassOf && map.has(c.subClassOf)) {
|
|
247
|
+
map.get(c.subClassOf).children.push(c.compact);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const c of classes) {
|
|
251
|
+
const chain = [];
|
|
252
|
+
let current = c.subClassOf;
|
|
253
|
+
while (current && map.has(current)) {
|
|
254
|
+
chain.push(current);
|
|
255
|
+
current = map.get(current).subClassOf;
|
|
256
|
+
}
|
|
257
|
+
if (current) chain.push(current);
|
|
258
|
+
c.ancestors = chain;
|
|
259
|
+
}
|
|
260
|
+
const roots = classes.filter((c) => !c.subClassOf || !map.has(c.subClassOf)).map((c) => c.compact);
|
|
261
|
+
return { roots, map: Object.fromEntries(map) };
|
|
262
|
+
}
|
|
263
|
+
function groupPropertiesByDomain(properties) {
|
|
264
|
+
const groups = {};
|
|
265
|
+
for (const p of properties) {
|
|
266
|
+
const domains = p.domainUnion || (p.domain ? [p.domain] : ["(unspecified)"]);
|
|
267
|
+
for (const d of domains) {
|
|
268
|
+
if (!groups[d]) groups[d] = { object: [], datatype: [] };
|
|
269
|
+
groups[d][p.type].push(p.compact);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return groups;
|
|
273
|
+
}
|
|
274
|
+
function extractPropertyBlocks(shapeBlock) {
|
|
275
|
+
const blocks = [];
|
|
276
|
+
const re = /sh:property\s+\[/g;
|
|
277
|
+
let match;
|
|
278
|
+
while ((match = re.exec(shapeBlock)) !== null) {
|
|
279
|
+
const start = match.index + match[0].length - 1;
|
|
280
|
+
let depth = 1;
|
|
281
|
+
let i = start + 1;
|
|
282
|
+
while (i < shapeBlock.length && depth > 0) {
|
|
283
|
+
if (shapeBlock[i] === "[") depth++;
|
|
284
|
+
else if (shapeBlock[i] === "]") depth--;
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
blocks.push(shapeBlock.slice(start + 1, i - 1));
|
|
288
|
+
}
|
|
289
|
+
return blocks;
|
|
290
|
+
}
|
|
291
|
+
function parseConstraint(propBlock) {
|
|
292
|
+
const c = {};
|
|
293
|
+
const path = extractResource(propBlock, "sh:path");
|
|
294
|
+
c.path = path ? compactIri(expandPrefixed(path)) : null;
|
|
295
|
+
const dt = extractResource(propBlock, "sh:datatype");
|
|
296
|
+
c.datatype = dt ? compactIri(expandPrefixed(dt)) : null;
|
|
297
|
+
const cls = extractResource(propBlock, "sh:class");
|
|
298
|
+
c.class = cls ? compactIri(expandPrefixed(cls)) : null;
|
|
299
|
+
const vf = extractResource(propBlock, "sh:valuesFrom");
|
|
300
|
+
c.valuesFrom = vf ? compactIri(expandPrefixed(vf)) : null;
|
|
301
|
+
const nk = extractResource(propBlock, "sh:nodeKind");
|
|
302
|
+
c.nodeKind = nk ? compactIri(expandPrefixed(nk)) : null;
|
|
303
|
+
const minMatch = propBlock.match(/sh:minCount\s+(\d+)/);
|
|
304
|
+
c.minCount = minMatch ? parseInt(minMatch[1], 10) : null;
|
|
305
|
+
const maxMatch = propBlock.match(/sh:maxCount\s+(\d+)/);
|
|
306
|
+
c.maxCount = maxMatch ? parseInt(maxMatch[1], 10) : null;
|
|
307
|
+
const inMatch = propBlock.match(/sh:in\s*\(([^)]+)\)/);
|
|
308
|
+
if (inMatch) {
|
|
309
|
+
c.in = inMatch[1].match(/"([^"]+)"/g)?.map((s) => s.replace(/"/g, "")) || null;
|
|
310
|
+
} else {
|
|
311
|
+
c.in = null;
|
|
312
|
+
}
|
|
313
|
+
return c;
|
|
314
|
+
}
|
|
315
|
+
function parseShaclShapes(ttlText) {
|
|
316
|
+
const cleaned = preprocessTtl(ttlText);
|
|
317
|
+
const blocks = splitSubjectBlocks(cleaned);
|
|
318
|
+
const shapes = [];
|
|
319
|
+
for (const block of blocks) {
|
|
320
|
+
const trimmed = block.trim();
|
|
321
|
+
if (!trimmed) continue;
|
|
322
|
+
const subjectMatch = trimmed.match(/^([^\s]+)/);
|
|
323
|
+
if (!subjectMatch) continue;
|
|
324
|
+
const subject = subjectMatch[1];
|
|
325
|
+
if (!/\ba\s+sh:NodeShape\b/.test(trimmed)) continue;
|
|
326
|
+
const iri = expandPrefixed(subject);
|
|
327
|
+
const compact = compactIri(iri);
|
|
328
|
+
const label = extractLiteral(trimmed, "rdfs:label");
|
|
329
|
+
const comment = extractLiteral(trimmed, "rdfs:comment");
|
|
330
|
+
const targetClass = extractResource(trimmed, "sh:targetClass");
|
|
331
|
+
const propBlocks = extractPropertyBlocks(trimmed);
|
|
332
|
+
const constraints = propBlocks.map(parseConstraint).filter((c) => c.path);
|
|
333
|
+
const shapeClassMatch = trimmed.match(/^\s*[^[]*?\ba\s+sh:NodeShape\s[^[]*?sh:class\s+([^\s,;]+)/);
|
|
334
|
+
const shapeClass = shapeClassMatch ? shapeClassMatch[1].replace(/[;.]+$/, "") : null;
|
|
335
|
+
shapes.push({
|
|
336
|
+
iri,
|
|
337
|
+
compact,
|
|
338
|
+
label: label || subject.replace("gloss:", "").replace("Shape", ""),
|
|
339
|
+
comment,
|
|
340
|
+
targetClass: targetClass ? compactIri(expandPrefixed(targetClass)) : null,
|
|
341
|
+
shapeClass: shapeClass ? compactIri(expandPrefixed(shapeClass)) : null,
|
|
342
|
+
constraints
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return shapes;
|
|
346
|
+
}
|
|
347
|
+
var IMPORT_LABELS = {
|
|
348
|
+
"http://www.w3.org/2004/02/skos/core": "SKOS",
|
|
349
|
+
"http://www.w3.org/2004/02/skos/core#": "SKOS",
|
|
350
|
+
"http://www.w3.org/2008/05/skos-xl": "SKOS-XL",
|
|
351
|
+
"http://www.w3.org/2008/05/skos-xl#": "SKOS-XL",
|
|
352
|
+
"http://purl.org/iso25964/skos-thes": "ISO 25964",
|
|
353
|
+
"http://purl.org/iso25964/skos-thes#": "ISO 25964",
|
|
354
|
+
"http://www.w3.org/ns/prov#": "PROV-O",
|
|
355
|
+
"http://purl.org/dc/terms/": "Dublin Core Terms"
|
|
356
|
+
};
|
|
357
|
+
function parseOntologyDeclaration(ttlText) {
|
|
358
|
+
const cleaned = preprocessTtl(ttlText);
|
|
359
|
+
const blocks = splitSubjectBlocks(cleaned);
|
|
360
|
+
for (const block of blocks) {
|
|
361
|
+
const trimmed = block.trim();
|
|
362
|
+
if (!trimmed) continue;
|
|
363
|
+
if (!/\ba\s+owl:Ontology\b/.test(trimmed)) continue;
|
|
364
|
+
const subjectMatch = trimmed.match(/^<([^>]+)>/);
|
|
365
|
+
const iri = subjectMatch ? subjectMatch[1] : null;
|
|
366
|
+
const label = extractLiteral(trimmed, "rdfs:label") || extractLiteral(trimmed, "dcterms:title");
|
|
367
|
+
const comment = extractLiteral(trimmed, "rdfs:comment") || extractLiteral(trimmed, "dcterms:description");
|
|
368
|
+
const prefix = extractLiteral(trimmed, "vann:preferredNamespacePrefix");
|
|
369
|
+
const nsUri = extractLiteral(trimmed, "vann:preferredNamespaceUri");
|
|
370
|
+
const license = extractResource(trimmed, "dcterms:license")?.replace(/[<>]/g, "") || null;
|
|
371
|
+
const created = extractLiteral(trimmed, "dcterms:created");
|
|
372
|
+
const imports = [];
|
|
373
|
+
const importRe = /owl:imports\s+<([^>]+)>/g;
|
|
374
|
+
let im;
|
|
375
|
+
while ((im = importRe.exec(trimmed)) !== null) {
|
|
376
|
+
const iri2 = im[1];
|
|
377
|
+
imports.push({
|
|
378
|
+
iri: iri2,
|
|
379
|
+
label: IMPORT_LABELS[iri2] || IMPORT_LABELS[iri2.replace(/#?$/, "#")] || compactIri(iri2)
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return { iri, label, comment, prefix, namespaceUri: nsUri, imports, license, created };
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
function parseAnnotationProperties(ttlText) {
|
|
387
|
+
const cleaned = preprocessTtl(ttlText);
|
|
388
|
+
const blocks = splitSubjectBlocks(cleaned);
|
|
389
|
+
const props = [];
|
|
390
|
+
for (const block of blocks) {
|
|
391
|
+
const trimmed = block.trim();
|
|
392
|
+
if (!trimmed) continue;
|
|
393
|
+
if (!/\ba\s+owl:AnnotationProperty\b/.test(trimmed)) continue;
|
|
394
|
+
const subjectMatch = trimmed.match(/^([^\s]+)/);
|
|
395
|
+
if (!subjectMatch) continue;
|
|
396
|
+
const subject = subjectMatch[1];
|
|
397
|
+
const iri = expandPrefixed(subject);
|
|
398
|
+
const compact = compactIri(iri);
|
|
399
|
+
const label = extractLiteral(trimmed, "rdfs:label");
|
|
400
|
+
props.push({ iri, compact, label: label || compact });
|
|
401
|
+
}
|
|
402
|
+
return props;
|
|
403
|
+
}
|
|
404
|
+
function main() {
|
|
405
|
+
if (!existsSync(ONTOLOGY_TTL)) {
|
|
406
|
+
console.error(`Ontology file not found: ${ONTOLOGY_TTL}`);
|
|
407
|
+
console.error("Run `npm run sync:model` to vendor from glossarist/concept-model.");
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
const ttlText = readFileSync(ONTOLOGY_TTL, "utf-8");
|
|
411
|
+
const { classes, properties } = parseOntology(ttlText);
|
|
412
|
+
const hierarchy = buildClassHierarchy(classes);
|
|
413
|
+
const propsByDomain = groupPropertiesByDomain(properties);
|
|
414
|
+
let shapes = [];
|
|
415
|
+
if (existsSync(SHACL_TTL)) {
|
|
416
|
+
const shaclText = readFileSync(SHACL_TTL, "utf-8");
|
|
417
|
+
shapes = parseShaclShapes(shaclText);
|
|
418
|
+
} else {
|
|
419
|
+
console.warn(`SHACL shapes file not found: ${SHACL_TTL}`);
|
|
420
|
+
}
|
|
421
|
+
const ontologyDecl = parseOntologyDeclaration(ttlText);
|
|
422
|
+
let annotationProps = parseAnnotationProperties(ttlText);
|
|
423
|
+
if (annotationProps.length === 0) {
|
|
424
|
+
annotationProps = [
|
|
425
|
+
{ iri: "http://www.w3.org/2000/01/rdf-schema#label", compact: "rdfs:label", label: "label" },
|
|
426
|
+
{ iri: "http://www.w3.org/2000/01/rdf-schema#comment", compact: "rdfs:comment", label: "comment" },
|
|
427
|
+
{ iri: "http://www.w3.org/2000/01/rdf-schema#seeAlso", compact: "rdfs:seeAlso", label: "seeAlso" },
|
|
428
|
+
{ iri: "http://www.w3.org/2000/01/rdf-schema#isDefinedBy", compact: "rdfs:isDefinedBy", label: "isDefinedBy" },
|
|
429
|
+
{ iri: "http://purl.org/dc/terms/title", compact: "dcterms:title", label: "title" },
|
|
430
|
+
{ iri: "http://purl.org/dc/terms/description", compact: "dcterms:description", label: "description" },
|
|
431
|
+
{ iri: "http://purl.org/dc/terms/source", compact: "dcterms:source", label: "source" },
|
|
432
|
+
{ iri: "http://purl.org/dc/terms/license", compact: "dcterms:license", label: "license" },
|
|
433
|
+
{ iri: "http://purl.org/dc/terms/created", compact: "dcterms:created", label: "created" },
|
|
434
|
+
{ iri: "http://purl.org/vocab/vann/preferredNamespacePrefix", compact: "vann:preferredNamespacePrefix", label: "preferredNamespacePrefix" },
|
|
435
|
+
{ iri: "http://purl.org/vocab/vann/preferredNamespaceUri", compact: "vann:preferredNamespaceUri", label: "preferredNamespaceUri" }
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
const shapesByTargetClass = {};
|
|
439
|
+
for (const s of shapes) {
|
|
440
|
+
const tc = s.targetClass || "(unspecified)";
|
|
441
|
+
if (!shapesByTargetClass[tc]) shapesByTargetClass[tc] = [];
|
|
442
|
+
shapesByTargetClass[tc].push(s.compact);
|
|
443
|
+
}
|
|
444
|
+
const output = {
|
|
445
|
+
ontology: ontologyDecl,
|
|
446
|
+
ontologyIri: ontologyDecl?.iri || "https://www.glossarist.org/ontologies/glossarist",
|
|
447
|
+
ontologyLabel: ontologyDecl?.label || "Glossarist Ontology",
|
|
448
|
+
classes: hierarchy.map,
|
|
449
|
+
classHierarchyRoots: hierarchy.roots,
|
|
450
|
+
properties: Object.fromEntries(properties.map((p) => [p.compact, p])),
|
|
451
|
+
propertiesByDomain: propsByDomain,
|
|
452
|
+
shapes: Object.fromEntries(shapes.map((s) => [s.compact, s])),
|
|
453
|
+
shapesByTargetClass,
|
|
454
|
+
annotationProperties: annotationProps,
|
|
455
|
+
stats: {
|
|
456
|
+
classCount: classes.length,
|
|
457
|
+
objectPropertyCount: properties.filter((p) => p.type === "object").length,
|
|
458
|
+
datatypePropertyCount: properties.filter((p) => p.type === "datatype").length,
|
|
459
|
+
shapeCount: shapes.length,
|
|
460
|
+
annotationPropertyCount: annotationProps.length
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
mkdirSync(dirname(OUTPUT), { recursive: true });
|
|
464
|
+
writeFileSync(OUTPUT, JSON.stringify(output, null, 2) + "\n");
|
|
465
|
+
console.log(`Parsed ${output.stats.classCount} classes, ${output.stats.objectPropertyCount} object properties, ${output.stats.datatypePropertyCount} datatype properties, ${output.stats.shapeCount} SHACL shapes, ${output.stats.annotationPropertyCount} annotation properties`);
|
|
466
|
+
if (ontologyDecl) {
|
|
467
|
+
console.log(`Ontology: ${ontologyDecl.iri}`);
|
|
468
|
+
console.log(` imports: ${ontologyDecl.imports.map((i) => i.label).join(", ")}`);
|
|
469
|
+
}
|
|
470
|
+
console.log(`Wrote ${OUTPUT}`);
|
|
471
|
+
}
|
|
472
|
+
main();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/load-site-config.ts
|
|
4
|
+
import { readFileSync, existsSync } from "fs";
|
|
5
|
+
import { resolve, dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
var projectRoot = resolve(__dirname, "..");
|
|
10
|
+
var SNAKE_TO_CAMEL = {
|
|
11
|
+
// Site-level
|
|
12
|
+
uri_base: "uriBase",
|
|
13
|
+
base_path: "basePath",
|
|
14
|
+
ui_languages: "uiLanguages",
|
|
15
|
+
dataset_groups: "datasetGroups",
|
|
16
|
+
dataset_translations: "datasetTranslations",
|
|
17
|
+
primary_color: "primaryColor",
|
|
18
|
+
dark_color: "darkColor",
|
|
19
|
+
owner_name: "ownerName",
|
|
20
|
+
owner_url: "ownerUrl",
|
|
21
|
+
powered_by: "poweredBy",
|
|
22
|
+
footer_nav: "footerNav",
|
|
23
|
+
footer_logo: "footerLogo",
|
|
24
|
+
news_dir: "newsDir",
|
|
25
|
+
// Dataset-level
|
|
26
|
+
gcr_package: "gcrPackage",
|
|
27
|
+
source_repo: "sourceRepo",
|
|
28
|
+
local_path: "localPath",
|
|
29
|
+
local_light: "localLight",
|
|
30
|
+
local_dark: "localDark",
|
|
31
|
+
uri_aliases: "uriAliases",
|
|
32
|
+
language_order: "languageOrder",
|
|
33
|
+
ref_aliases: "refAliases",
|
|
34
|
+
urn_aliases: "urnAliases",
|
|
35
|
+
existing_site_url: "existingSiteUrl",
|
|
36
|
+
edition_status: "editionStatus",
|
|
37
|
+
dataset_uri: "datasetUri",
|
|
38
|
+
schema_version: "schemaVersion"
|
|
39
|
+
};
|
|
40
|
+
function normalizeKey(key) {
|
|
41
|
+
if (typeof key !== "string") return key;
|
|
42
|
+
if (Object.prototype.hasOwnProperty.call(SNAKE_TO_CAMEL, key)) {
|
|
43
|
+
return SNAKE_TO_CAMEL[key];
|
|
44
|
+
}
|
|
45
|
+
if (key.includes("_") && /^[a-z][a-z0-9_]*$/.test(key)) {
|
|
46
|
+
return key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
47
|
+
}
|
|
48
|
+
return key;
|
|
49
|
+
}
|
|
50
|
+
function normalizeObject(obj) {
|
|
51
|
+
if (Array.isArray(obj)) return obj.map(normalizeObject);
|
|
52
|
+
if (obj && typeof obj === "object") {
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
55
|
+
out[normalizeKey(k)] = normalizeObject(v);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
return obj;
|
|
60
|
+
}
|
|
61
|
+
function findConfigFile(args = []) {
|
|
62
|
+
if (process.env.SITE_CONFIG) {
|
|
63
|
+
return resolve(process.env.SITE_CONFIG);
|
|
64
|
+
}
|
|
65
|
+
for (const dir of [process.cwd(), projectRoot]) {
|
|
66
|
+
const p = resolve(dir, "site-config.yml");
|
|
67
|
+
if (existsSync(p)) return p;
|
|
68
|
+
}
|
|
69
|
+
throw new Error("No site config found. Set SITE_CONFIG or create site-config.yml");
|
|
70
|
+
}
|
|
71
|
+
function loadSiteConfig(args = []) {
|
|
72
|
+
const configPath = findConfigFile(args);
|
|
73
|
+
const raw = yaml.load(readFileSync(configPath, "utf-8"));
|
|
74
|
+
const config = normalizeObject(raw);
|
|
75
|
+
return { config, configPath };
|
|
76
|
+
}
|
|
77
|
+
function getProjectRoot() {
|
|
78
|
+
return projectRoot;
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
getProjectRoot,
|
|
82
|
+
loadSiteConfig
|
|
83
|
+
};
|