@glossarist/concept-browser 0.7.115 → 0.7.117
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 +8 -4
- package/scripts/bridge-to-astro.js +132 -0
- package/scripts/build-edges.js +480 -0
- package/scripts/build-scripts.mjs +31 -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/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 +100 -0
- package/src/__tests__/scripts-syntax.test.ts +2 -2
- package/src/data/ontology-schema.json +1336 -183
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/migrate-v1-to-v3.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
function isV1PartitiveRelation(rel) {
|
|
7
|
+
return rel && Array.isArray(rel.parts);
|
|
8
|
+
}
|
|
9
|
+
function migrateRelation(rel) {
|
|
10
|
+
if (!isV1PartitiveRelation(rel)) return null;
|
|
11
|
+
return {
|
|
12
|
+
comprehensive: rel.comprehensive,
|
|
13
|
+
completeness: rel.enumeration === "open" ? "partial" : rel.enumeration === "closed" || rel.enumeration == null ? "complete" : rel.enumeration,
|
|
14
|
+
partitives: (rel.parts ?? []).map((p) => ({
|
|
15
|
+
ref: p,
|
|
16
|
+
certainty: "confirmed"
|
|
17
|
+
})),
|
|
18
|
+
...rel.content ? { content: rel.content } : {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function migrateConceptData(data) {
|
|
22
|
+
if (!data?.partitive_relations?.length) return null;
|
|
23
|
+
const migrated = data.partitive_relations.map(migrateRelation).filter((r) => r !== null);
|
|
24
|
+
if (migrated.length === 0) return null;
|
|
25
|
+
return { ...data, partitive_relations: migrated };
|
|
26
|
+
}
|
|
27
|
+
function isAlreadyMigrated(content) {
|
|
28
|
+
return Boolean(content?.data?.partitive_relations?.some?.((rel) => Array.isArray(rel.partitives)));
|
|
29
|
+
}
|
|
30
|
+
function walkFiles(input) {
|
|
31
|
+
const stat = fs.statSync(input);
|
|
32
|
+
if (stat.isFile()) return [input];
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const entry of fs.readdirSync(input, { withFileTypes: true })) {
|
|
35
|
+
const full = path.join(input, entry.name);
|
|
36
|
+
if (entry.isDirectory()) out.push(...walkFiles(full));
|
|
37
|
+
else if (entry.isFile() && /\.(ya?ml)$/i.test(entry.name)) out.push(full);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
async function loadYamlModule() {
|
|
42
|
+
try {
|
|
43
|
+
return await import("js-yaml");
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function outputPath(input, file, outRoot) {
|
|
49
|
+
const stat = fs.statSync(input);
|
|
50
|
+
if (stat.isFile()) return outRoot;
|
|
51
|
+
const rel = path.relative(input, file);
|
|
52
|
+
return path.join(outRoot, rel);
|
|
53
|
+
}
|
|
54
|
+
async function migrateFile(input, file, options, yaml) {
|
|
55
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
56
|
+
const parsed = yaml ? yaml.load(raw) : JSON.parse(raw);
|
|
57
|
+
if (!parsed?.data) return { changed: false };
|
|
58
|
+
if (isAlreadyMigrated(parsed)) return { changed: false, skipped: true };
|
|
59
|
+
const migrated = migrateConceptData(parsed.data);
|
|
60
|
+
if (!migrated) return { changed: false };
|
|
61
|
+
parsed.data = { ...parsed.data, ...migrated };
|
|
62
|
+
const out = outputPath(input, file, options.out);
|
|
63
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
64
|
+
const outText = yaml ? yaml.dump(parsed) : JSON.stringify(parsed, null, 2);
|
|
65
|
+
if (!options.dryRun) fs.writeFileSync(out, outText);
|
|
66
|
+
return { changed: true, out, in: file };
|
|
67
|
+
}
|
|
68
|
+
async function main() {
|
|
69
|
+
const args = process.argv.slice(2);
|
|
70
|
+
const opts = { in: null, out: null, dryRun: false };
|
|
71
|
+
for (let i = 0; i < args.length; i++) {
|
|
72
|
+
const a = args[i];
|
|
73
|
+
if (a === "--in") opts.in = args[++i];
|
|
74
|
+
else if (a === "--out") opts.out = args[++i];
|
|
75
|
+
else if (a === "--dry-run") opts.dryRun = true;
|
|
76
|
+
else if (!opts.in) opts.in = a;
|
|
77
|
+
else if (!opts.out) opts.out = a;
|
|
78
|
+
}
|
|
79
|
+
if (!opts.in) {
|
|
80
|
+
console.error("Usage: node scripts/migrate-v1-to-v3.mjs --in <dir|file> [--out <dir|file>] [--dry-run]");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
if (!opts.out) opts.out = opts.dryRun ? opts.in : `${opts.in}.v3`;
|
|
84
|
+
const yaml = await loadYamlModule();
|
|
85
|
+
if (!yaml) {
|
|
86
|
+
console.error("js-yaml is required; install via npm install js-yaml");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const files = walkFiles(opts.in);
|
|
90
|
+
let changed = 0, skipped = 0, total = 0;
|
|
91
|
+
for (const f of files) {
|
|
92
|
+
total++;
|
|
93
|
+
const res = await migrateFile(opts.in, f, opts, yaml);
|
|
94
|
+
if (res.changed) {
|
|
95
|
+
changed++;
|
|
96
|
+
console.log(`\u2713 ${res.in} \u2192 ${res.out}`);
|
|
97
|
+
} else if (res.skipped) {
|
|
98
|
+
skipped++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log(`
|
|
102
|
+
${changed} migrated, ${skipped} skipped (already v3), ${total - changed - skipped} unchanged.`);
|
|
103
|
+
if (opts.dryRun) console.log("(dry-run \u2014 no files written)");
|
|
104
|
+
}
|
|
105
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
106
|
+
main();
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
isAlreadyMigrated,
|
|
110
|
+
isV1PartitiveRelation,
|
|
111
|
+
migrateConceptData,
|
|
112
|
+
migrateRelation
|
|
113
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/normalize-yaml.ts
|
|
4
|
+
import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
5
|
+
import { join, relative, extname, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
var ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
var YAML_EXTS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
9
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
10
|
+
function walk(dir) {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const entry of readdirSync(dir)) {
|
|
13
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
14
|
+
const full = join(dir, entry);
|
|
15
|
+
const st = statSync(full);
|
|
16
|
+
if (st.isDirectory()) {
|
|
17
|
+
out.push(...walk(full));
|
|
18
|
+
} else if (YAML_EXTS.has(extname(entry))) {
|
|
19
|
+
out.push(full);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function normalizeYaml({ root = ROOT, check = false, paths = [] } = {}) {
|
|
25
|
+
const searchDirs = paths.length > 0 ? paths.map((p) => join(root, p)) : [join(root, ".datasets")];
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const dir of searchDirs) {
|
|
28
|
+
try {
|
|
29
|
+
statSync(dir);
|
|
30
|
+
} catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
files.push(...walk(dir));
|
|
34
|
+
}
|
|
35
|
+
files.sort();
|
|
36
|
+
let checked = 0;
|
|
37
|
+
let nonNfc = 0;
|
|
38
|
+
const fixed = [];
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const original = readFileSync(file, "utf8");
|
|
41
|
+
checked++;
|
|
42
|
+
if (!isNfc(original)) {
|
|
43
|
+
nonNfc++;
|
|
44
|
+
if (!check) {
|
|
45
|
+
writeFileSync(file, original.normalize("NFC"), "utf8");
|
|
46
|
+
fixed.push(relative(root, file));
|
|
47
|
+
} else {
|
|
48
|
+
fixed.push(relative(root, file));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { checked, nonNfc, fixed, check };
|
|
53
|
+
}
|
|
54
|
+
function isNfc(s) {
|
|
55
|
+
return s === s.normalize("NFC");
|
|
56
|
+
}
|
|
57
|
+
function main() {
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
const check = args.includes("--check");
|
|
60
|
+
const paths = args.filter((a) => !a.startsWith("-") && a !== "normalize");
|
|
61
|
+
const { checked, nonNfc, fixed } = normalizeYaml({ check, paths });
|
|
62
|
+
if (check) {
|
|
63
|
+
if (nonNfc === 0) {
|
|
64
|
+
process.stdout.write(`NFC OK: ${checked} file(s) checked, all normalized
|
|
65
|
+
`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
process.stderr.write(`NFC check failed: ${nonNfc} of ${checked} file(s) are not NFC-normalized
|
|
69
|
+
|
|
70
|
+
`);
|
|
71
|
+
for (const f of fixed) {
|
|
72
|
+
process.stderr.write(` ${f}
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
process.exit(1);
|
|
76
|
+
} else {
|
|
77
|
+
if (nonNfc === 0) {
|
|
78
|
+
process.stdout.write(`NFC OK: ${checked} file(s) checked, all already normalized
|
|
79
|
+
`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(`Normalized ${nonNfc} of ${checked} file(s)
|
|
83
|
+
`);
|
|
84
|
+
for (const f of fixed) {
|
|
85
|
+
process.stdout.write(` ${f}
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var isDirectEntry = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
91
|
+
if (isDirectEntry) {
|
|
92
|
+
main();
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
normalizeYaml
|
|
96
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/process-about-pages.ts
|
|
4
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync, readdirSync, statSync, realpathSync } from "node:fs";
|
|
5
|
+
import { join, basename, extname, resolve as resolve2 } from "node:path";
|
|
6
|
+
import { cwd } from "node:process";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8
|
+
|
|
9
|
+
// scripts/load-site-config.ts
|
|
10
|
+
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
import { resolve, dirname } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import yaml from "js-yaml";
|
|
14
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
var projectRoot = resolve(__dirname, "..");
|
|
16
|
+
var SNAKE_TO_CAMEL = {
|
|
17
|
+
// Site-level
|
|
18
|
+
uri_base: "uriBase",
|
|
19
|
+
base_path: "basePath",
|
|
20
|
+
ui_languages: "uiLanguages",
|
|
21
|
+
dataset_groups: "datasetGroups",
|
|
22
|
+
dataset_translations: "datasetTranslations",
|
|
23
|
+
primary_color: "primaryColor",
|
|
24
|
+
dark_color: "darkColor",
|
|
25
|
+
owner_name: "ownerName",
|
|
26
|
+
owner_url: "ownerUrl",
|
|
27
|
+
powered_by: "poweredBy",
|
|
28
|
+
footer_nav: "footerNav",
|
|
29
|
+
footer_logo: "footerLogo",
|
|
30
|
+
news_dir: "newsDir",
|
|
31
|
+
// Dataset-level
|
|
32
|
+
gcr_package: "gcrPackage",
|
|
33
|
+
source_repo: "sourceRepo",
|
|
34
|
+
local_path: "localPath",
|
|
35
|
+
local_light: "localLight",
|
|
36
|
+
local_dark: "localDark",
|
|
37
|
+
uri_aliases: "uriAliases",
|
|
38
|
+
language_order: "languageOrder",
|
|
39
|
+
ref_aliases: "refAliases",
|
|
40
|
+
urn_aliases: "urnAliases",
|
|
41
|
+
existing_site_url: "existingSiteUrl",
|
|
42
|
+
edition_status: "editionStatus",
|
|
43
|
+
dataset_uri: "datasetUri",
|
|
44
|
+
schema_version: "schemaVersion"
|
|
45
|
+
};
|
|
46
|
+
function normalizeKey(key) {
|
|
47
|
+
if (typeof key !== "string") return key;
|
|
48
|
+
if (Object.prototype.hasOwnProperty.call(SNAKE_TO_CAMEL, key)) {
|
|
49
|
+
return SNAKE_TO_CAMEL[key];
|
|
50
|
+
}
|
|
51
|
+
if (key.includes("_") && /^[a-z][a-z0-9_]*$/.test(key)) {
|
|
52
|
+
return key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
53
|
+
}
|
|
54
|
+
return key;
|
|
55
|
+
}
|
|
56
|
+
function normalizeObject(obj) {
|
|
57
|
+
if (Array.isArray(obj)) return obj.map(normalizeObject);
|
|
58
|
+
if (obj && typeof obj === "object") {
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
61
|
+
out[normalizeKey(k)] = normalizeObject(v);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
return obj;
|
|
66
|
+
}
|
|
67
|
+
function findConfigFile(args = []) {
|
|
68
|
+
if (process.env.SITE_CONFIG) {
|
|
69
|
+
return resolve(process.env.SITE_CONFIG);
|
|
70
|
+
}
|
|
71
|
+
for (const dir of [process.cwd(), projectRoot]) {
|
|
72
|
+
const p = resolve(dir, "site-config.yml");
|
|
73
|
+
if (existsSync(p)) return p;
|
|
74
|
+
}
|
|
75
|
+
throw new Error("No site config found. Set SITE_CONFIG or create site-config.yml");
|
|
76
|
+
}
|
|
77
|
+
function loadSiteConfig(args = []) {
|
|
78
|
+
const configPath = findConfigFile(args);
|
|
79
|
+
const raw = yaml.load(readFileSync(configPath, "utf-8"));
|
|
80
|
+
const config = normalizeObject(raw);
|
|
81
|
+
return { config, configPath };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// scripts/process-about-pages.ts
|
|
85
|
+
function loadSiteConfigForAbout() {
|
|
86
|
+
try {
|
|
87
|
+
const { config } = loadSiteConfig([]);
|
|
88
|
+
return config;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function roots() {
|
|
94
|
+
const ROOT = cwd();
|
|
95
|
+
return {
|
|
96
|
+
ROOT,
|
|
97
|
+
PUBLIC_PAGES: join(ROOT, "public", "pages"),
|
|
98
|
+
DATASETS_DIR: join(ROOT, ".datasets"),
|
|
99
|
+
GROUPS_CONTENT_DIR: join(ROOT, "site-content", "groups")
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function renderMarkdown(text) {
|
|
103
|
+
const lines = text.split("\n");
|
|
104
|
+
const html = [];
|
|
105
|
+
let inList = false;
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
if (!trimmed) {
|
|
109
|
+
if (inList) {
|
|
110
|
+
html.push("</ul>");
|
|
111
|
+
inList = false;
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (/^#{1}\s/.test(trimmed)) {
|
|
116
|
+
if (inList) {
|
|
117
|
+
html.push("</ul>");
|
|
118
|
+
inList = false;
|
|
119
|
+
}
|
|
120
|
+
html.push(`<h1>${inline(trimmed.replace(/^#\s/, ""))}</h1>`);
|
|
121
|
+
} else if (/^#{2}\s/.test(trimmed)) {
|
|
122
|
+
if (inList) {
|
|
123
|
+
html.push("</ul>");
|
|
124
|
+
inList = false;
|
|
125
|
+
}
|
|
126
|
+
html.push(`<h2>${inline(trimmed.replace(/^##\s/, ""))}</h2>`);
|
|
127
|
+
} else if (/^#{3}\s/.test(trimmed)) {
|
|
128
|
+
if (inList) {
|
|
129
|
+
html.push("</ul>");
|
|
130
|
+
inList = false;
|
|
131
|
+
}
|
|
132
|
+
html.push(`<h3>${inline(trimmed.replace(/^###\s/, ""))}</h3>`);
|
|
133
|
+
} else if (/^[-*]\s/.test(trimmed)) {
|
|
134
|
+
if (!inList) {
|
|
135
|
+
html.push("<ul>");
|
|
136
|
+
inList = true;
|
|
137
|
+
}
|
|
138
|
+
html.push(`<li>${inline(trimmed.replace(/^[-*]\s/, ""))}</li>`);
|
|
139
|
+
} else {
|
|
140
|
+
if (inList) {
|
|
141
|
+
html.push("</ul>");
|
|
142
|
+
inList = false;
|
|
143
|
+
}
|
|
144
|
+
html.push(`<p>${inline(trimmed)}</p>`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (inList) html.push("</ul>");
|
|
148
|
+
return html.join("\n");
|
|
149
|
+
}
|
|
150
|
+
function inline(text) {
|
|
151
|
+
return text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\*(.+?)\*/g, "<em>$1</em>").replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>').replace(/`(.+?)`/g, "<code>$1</code>");
|
|
152
|
+
}
|
|
153
|
+
function processAboutDir(sourceDir, outputPrefix, publicPagesDir) {
|
|
154
|
+
if (!existsSync2(sourceDir)) return 0;
|
|
155
|
+
let count = 0;
|
|
156
|
+
for (const file of readdirSync(sourceDir)) {
|
|
157
|
+
const fullPath = join(sourceDir, file);
|
|
158
|
+
if (!statSync(fullPath).isFile()) continue;
|
|
159
|
+
const ext = extname(file);
|
|
160
|
+
if (ext !== ".md" && ext !== ".adoc" && ext !== ".html") continue;
|
|
161
|
+
const base = basename(file, ext);
|
|
162
|
+
const langMatch = base.match(/^about\.(\w+)$/);
|
|
163
|
+
const lang = langMatch ? langMatch[1] : "eng";
|
|
164
|
+
const outputName = lang === "eng" ? `${outputPrefix}.json` : `${outputPrefix}.${lang}.json`;
|
|
165
|
+
const raw = readFileSync2(fullPath, "utf8");
|
|
166
|
+
let html;
|
|
167
|
+
let title;
|
|
168
|
+
if (ext === ".html") {
|
|
169
|
+
html = raw;
|
|
170
|
+
const titleMatch = raw.match(/<h1[^>]*>(.+?)<\/h1>/);
|
|
171
|
+
title = titleMatch ? titleMatch[1] : "About";
|
|
172
|
+
} else if (ext === ".md") {
|
|
173
|
+
html = renderMarkdown(raw);
|
|
174
|
+
const titleMatch = raw.match(/^#\s+(.+)$/m);
|
|
175
|
+
title = titleMatch ? titleMatch[1] : "About";
|
|
176
|
+
} else {
|
|
177
|
+
html = `<p>${raw.split("\n").filter((l) => l.trim() && !l.startsWith("=")).join("</p>\n<p>")}</p>`;
|
|
178
|
+
const titleMatch = raw.match(/^=\s+(.+)$/m);
|
|
179
|
+
title = titleMatch ? titleMatch[1] : "About";
|
|
180
|
+
}
|
|
181
|
+
const output = { title, html };
|
|
182
|
+
mkdirSync(publicPagesDir, { recursive: true });
|
|
183
|
+
writeFileSync(join(publicPagesDir, outputName), JSON.stringify(output));
|
|
184
|
+
count++;
|
|
185
|
+
console.log(` Compiled: ${file} \u2192 public/pages/${outputName}`);
|
|
186
|
+
}
|
|
187
|
+
return count;
|
|
188
|
+
}
|
|
189
|
+
function main() {
|
|
190
|
+
const { ROOT, PUBLIC_PAGES, DATASETS_DIR, GROUPS_CONTENT_DIR } = roots();
|
|
191
|
+
mkdirSync(PUBLIC_PAGES, { recursive: true });
|
|
192
|
+
let total = 0;
|
|
193
|
+
const siteConfig = loadSiteConfigForAbout();
|
|
194
|
+
const localPathMap = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const ds of siteConfig?.datasets || []) {
|
|
196
|
+
if (ds.id && ds.localPath) {
|
|
197
|
+
localPathMap.set(ds.id, resolve2(ROOT, ds.localPath));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
201
|
+
if (existsSync2(DATASETS_DIR)) {
|
|
202
|
+
for (const dsId of readdirSync(DATASETS_DIR)) {
|
|
203
|
+
seenIds.add(dsId);
|
|
204
|
+
const aboutDir = join(DATASETS_DIR, dsId, "about");
|
|
205
|
+
if (existsSync2(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
206
|
+
console.log(`Processing dataset: ${dsId}`);
|
|
207
|
+
total += processAboutDir(aboutDir, `dataset-${dsId}-about`, PUBLIC_PAGES);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const [dsId, dsPath] of localPathMap) {
|
|
212
|
+
if (seenIds.has(dsId)) continue;
|
|
213
|
+
const aboutDir = join(dsPath, "about");
|
|
214
|
+
if (existsSync2(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
215
|
+
console.log(`Processing dataset (localPath): ${dsId}`);
|
|
216
|
+
total += processAboutDir(aboutDir, `dataset-${dsId}-about`, PUBLIC_PAGES);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (existsSync2(GROUPS_CONTENT_DIR)) {
|
|
220
|
+
for (const groupId of readdirSync(GROUPS_CONTENT_DIR)) {
|
|
221
|
+
const aboutDir = join(GROUPS_CONTENT_DIR, groupId, "about");
|
|
222
|
+
if (existsSync2(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
223
|
+
console.log(`Processing group: ${groupId}`);
|
|
224
|
+
total += processAboutDir(aboutDir, `group-${groupId}-about`, PUBLIC_PAGES);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
console.log(total > 0 ? `
|
|
229
|
+
Compiled ${total} about page(s).` : "\nNo about pages found.");
|
|
230
|
+
}
|
|
231
|
+
var isDirectInvocation = process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath2(import.meta.url));
|
|
232
|
+
if (isDirectInvocation) {
|
|
233
|
+
main();
|
|
234
|
+
}
|
|
235
|
+
export {
|
|
236
|
+
loadSiteConfigForAbout,
|
|
237
|
+
main,
|
|
238
|
+
processAboutDir,
|
|
239
|
+
renderMarkdown,
|
|
240
|
+
roots
|
|
241
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/sync-concept-model.ts
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
var ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
var OUT_DIR = resolve(ROOT, "data", "concept-model");
|
|
10
|
+
var REPO = "glossarist/concept-model";
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const out = { ref: null };
|
|
13
|
+
for (let i = 2; i < argv.length; i++) {
|
|
14
|
+
const a = argv[i];
|
|
15
|
+
if (a === "--ref") out.ref = argv[++i];
|
|
16
|
+
else if (!a.startsWith("--")) out.ref = a;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function latestTag() {
|
|
21
|
+
return execSync(`gh api repos/${REPO}/releases/latest --jq .tag_name`, { encoding: "utf8" }).trim();
|
|
22
|
+
}
|
|
23
|
+
function fetchFile(ref2, path) {
|
|
24
|
+
const url = `https://raw.githubusercontent.com/${REPO}/${ref2}/${path}`;
|
|
25
|
+
return execSync(`curl -fsSL ${url}`, { encoding: "utf8" });
|
|
26
|
+
}
|
|
27
|
+
var args = parseArgs(process.argv);
|
|
28
|
+
var ref = args.ref || latestTag();
|
|
29
|
+
if (!ref) {
|
|
30
|
+
console.error("Could not determine concept-model ref to sync.");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
console.log(`Syncing from ${REPO}@${ref}`);
|
|
34
|
+
mkdirSync(resolve(OUT_DIR, "shapes"), { recursive: true });
|
|
35
|
+
function fetchAny(ref2, candidates) {
|
|
36
|
+
for (const p of candidates) {
|
|
37
|
+
try {
|
|
38
|
+
return fetchFile(ref2, p);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Could not fetch any of: ${candidates.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
var targets = {
|
|
45
|
+
"glossarist.context.jsonld": ["ontologies/glossarist.context.jsonld", "glossarist.context.jsonld"],
|
|
46
|
+
"glossarist.ttl": ["ontologies/glossarist.ttl", "glossarist.ttl"],
|
|
47
|
+
"prefixes.ttl": ["ontologies/prefixes.ttl"],
|
|
48
|
+
"shapes/glossarist.shacl.ttl": ["ontologies/shapes/glossarist.shacl.ttl", "shapes/glossarist.shacl.ttl"]
|
|
49
|
+
};
|
|
50
|
+
var updated = 0;
|
|
51
|
+
for (const [outPath, candidates] of Object.entries(targets)) {
|
|
52
|
+
const content = fetchAny(ref, candidates);
|
|
53
|
+
writeFileSync(resolve(OUT_DIR, outPath), content);
|
|
54
|
+
console.log(` \u2713 ${outPath} (${content.length} bytes)`);
|
|
55
|
+
updated++;
|
|
56
|
+
}
|
|
57
|
+
writeFileSync(
|
|
58
|
+
resolve(OUT_DIR, "SOURCE.json"),
|
|
59
|
+
JSON.stringify({ repo: REPO, ref, syncedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
|
|
60
|
+
);
|
|
61
|
+
console.log(`
|
|
62
|
+
Synced ${updated} file(s) from ${REPO}@${ref}.`);
|
|
63
|
+
console.log("Review the diff, then commit.");
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/validate-shacl.ts
|
|
4
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
5
|
+
import { join, extname, dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Parser as N3Parser } from "n3";
|
|
8
|
+
import rdfDataset from "@rdfjs/dataset";
|
|
9
|
+
import { validateShacl } from "glossarist/rdf/shacl";
|
|
10
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
var VENDORED_SHAPES = resolve(__dirname, "..", "data", "concept-model", "shapes", "glossarist.shacl.ttl");
|
|
12
|
+
var createDataset = rdfDataset.dataset.bind(rdfDataset);
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const out = { dataRoot: "public/data", shapesPath: null };
|
|
15
|
+
for (let i = 2; i < argv.length; i++) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
if (arg === "--shapes") {
|
|
18
|
+
out.shapesPath = argv[++i];
|
|
19
|
+
if (!out.shapesPath) throw new Error("--shapes requires a path argument");
|
|
20
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
21
|
+
console.log(USAGE);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
} else {
|
|
24
|
+
out.dataRoot = arg;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (process.env.SHAPES_PATH && !out.shapesPath) {
|
|
28
|
+
out.shapesPath = process.env.SHAPES_PATH;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
var USAGE = `Usage: validate-shacl.mjs [options] [data-root]
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--shapes <path> Path to a SHACL shapes .ttl file (overrides the default
|
|
36
|
+
vendored shapes under data/concept-model/shapes/).
|
|
37
|
+
--help, -h Show this help.
|
|
38
|
+
|
|
39
|
+
Environment:
|
|
40
|
+
SHAPES_PATH Same as --shapes. Lower priority than the CLI flag.
|
|
41
|
+
|
|
42
|
+
Default data-root is public/data.
|
|
43
|
+
`;
|
|
44
|
+
function resolveShapesPath(cliPath) {
|
|
45
|
+
if (cliPath) return cliPath;
|
|
46
|
+
if (!statSync(VENDORED_SHAPES, { throwIfNoEntry: false })) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Vendored SHACL shapes not found at ${VENDORED_SHAPES}.
|
|
49
|
+
Run \`npm run sync:model\` to fetch them from glossarist/concept-model, or pass --shapes <path>, or set SHAPES_PATH=<path>.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return VENDORED_SHAPES;
|
|
53
|
+
}
|
|
54
|
+
function parseTurtle(text, baseIri) {
|
|
55
|
+
const parser = new N3Parser({ baseIRI: baseIri });
|
|
56
|
+
const out = createDataset();
|
|
57
|
+
return new Promise((resolve2, reject) => {
|
|
58
|
+
parser.parse(text, (err, quad) => {
|
|
59
|
+
if (err) reject(err);
|
|
60
|
+
else if (quad) out.add(quad);
|
|
61
|
+
else resolve2(out);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function* walkTtl(dir) {
|
|
66
|
+
for (const entry of readdirSync(dir)) {
|
|
67
|
+
const full = join(dir, entry);
|
|
68
|
+
if (statSync(full).isDirectory()) yield* walkTtl(full);
|
|
69
|
+
else if (extname(full) === ".ttl") yield full;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function formatViolation(v) {
|
|
73
|
+
const focus = v.focusNode?.value ?? "(unknown)";
|
|
74
|
+
const shape = v.shape?.value ?? "(unknown)";
|
|
75
|
+
const path = v.path?.value ?? "";
|
|
76
|
+
const message = v.message && v.message.length > 0 ? v.message.map((m) => m.value).join("; ") : "(no message)";
|
|
77
|
+
const pathPart = path ? `
|
|
78
|
+
path: ${path}` : "";
|
|
79
|
+
return ` shape: ${shape}${pathPart}
|
|
80
|
+
node: ${focus}
|
|
81
|
+
message: ${message}`;
|
|
82
|
+
}
|
|
83
|
+
async function main() {
|
|
84
|
+
const args = parseArgs(process.argv);
|
|
85
|
+
const shapesPath = resolveShapesPath(args.shapesPath);
|
|
86
|
+
let shapesDataset;
|
|
87
|
+
try {
|
|
88
|
+
const shapesText = readFileSync(shapesPath, "utf8");
|
|
89
|
+
shapesDataset = await parseTurtle(shapesText, `file://${shapesPath}`);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error(`Failed to load SHACL shapes from ${shapesPath}: ${e.message}`);
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
let statDir;
|
|
95
|
+
try {
|
|
96
|
+
statDir = statSync(args.dataRoot);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.error(`Data root not found: ${args.dataRoot}`);
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
if (!statDir.isDirectory()) {
|
|
102
|
+
console.error(`Data root is not a directory: ${args.dataRoot}`);
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
105
|
+
const files = [...walkTtl(args.dataRoot)];
|
|
106
|
+
if (files.length === 0) {
|
|
107
|
+
console.log(`No .ttl files under ${args.dataRoot} \u2014 SHACL gate skipped.`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const violations = [];
|
|
111
|
+
for (const path of files) {
|
|
112
|
+
let graph;
|
|
113
|
+
try {
|
|
114
|
+
const text = readFileSync(path, "utf8");
|
|
115
|
+
graph = await parseTurtle(text, `file://${path}`);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
violations.push({ path, parseError: e.message });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const report = await validateShacl(graph, { shapes: shapesDataset });
|
|
121
|
+
if (!report.conforms) {
|
|
122
|
+
for (const v of report.results) {
|
|
123
|
+
violations.push({ path, result: v });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (violations.length === 0) {
|
|
128
|
+
console.log(`SHACL validation passed \u2014 ${files.length} file(s) conform.`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.error(`SHACL validation FAILED \u2014 ${violations.length} violation(s) in ${files.length} file(s):
|
|
132
|
+
`);
|
|
133
|
+
let currentPath = null;
|
|
134
|
+
for (const v of violations) {
|
|
135
|
+
if (v.path !== currentPath) {
|
|
136
|
+
currentPath = v.path;
|
|
137
|
+
console.error(`
|
|
138
|
+
${v.path}`);
|
|
139
|
+
}
|
|
140
|
+
if (v.parseError) {
|
|
141
|
+
console.error(` PARSE ERROR: ${v.parseError}`);
|
|
142
|
+
} else {
|
|
143
|
+
console.error(formatViolation(v.result));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
console.error("");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
main().catch((e) => {
|
|
150
|
+
console.error(`validate-shacl: unexpected error: ${e.stack ?? e}`);
|
|
151
|
+
process.exit(2);
|
|
152
|
+
});
|