@glossarist/concept-browser 0.7.101 → 0.7.102
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 +14 -251
- package/cli/index.ts +289 -0
- package/package.json +3 -2
- package/src/__tests__/__fixtures__/concepts.ts +3 -3
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +1 -0
- package/src/__tests__/designation-relationship.test.ts +2 -2
- package/src/__tests__/perf/serialization-perf.test.ts +1 -0
- package/src/__tests__/use-rdf-document.test.ts +1 -0
- package/src/adapters/bibliography-adapter.ts +1 -1
- package/src/adapters/model-bridge.ts +15 -15
- package/src/adapters/non-verbal/figure-bridge.ts +2 -2
- package/src/adapters/non-verbal/formula-bridge.ts +2 -2
- package/src/adapters/non-verbal/table-bridge.ts +2 -2
- package/src/components/CitationDisplay.vue +1 -1
- package/src/components/ConceptDetail.vue +2 -2
- package/src/components/ConceptDiffModal.vue +2 -2
- package/src/components/ConceptDiffView.vue +1 -1
- package/src/components/DesignationList.vue +4 -4
- package/src/components/NavIcon.vue +1 -0
- package/src/components/figure/FigureDisplay.vue +5 -5
- package/src/components/formula/FormulaDisplay.vue +4 -4
- package/src/components/table/TableDisplay.vue +3 -3
- package/src/components/use-rdf-document.ts +16 -7
- package/src/composables/use-concept-content.ts +3 -3
- package/src/composables/use-concept-diff.ts +2 -2
- package/src/composables/use-concept-edges.ts +3 -3
- package/src/config/page-types.ts +1 -1
- package/src/i18n/locales/eng.yml +12 -0
- package/src/i18n/locales/fra.yml +12 -0
- package/src/layouts/Default.astro +32 -2
- package/src/utils/content-renderer.ts +1 -1
- package/src/utils/partitive-multiplicity.ts +3 -2
- package/src/views/ConceptView.vue +3 -3
- package/src/views/learn/LearnRelationshipsView.vue +63 -0
- package/tailwind.config.js +3 -3
- package/tsconfig.json +1 -1
- package/src/adapters/model-bridge/bridges.ts +0 -159
- package/src/adapters/model-bridge/concept.ts +0 -97
- package/src/adapters/model-bridge/entity-refs.ts +0 -49
- package/src/adapters/model-bridge/hyperedge.ts +0 -103
- package/src/adapters/model-bridge/index.ts +0 -46
- package/src/adapters/model-bridge/mappers.ts +0 -277
- package/src/adapters/model-bridge/partitive.ts +0 -79
- package/src/adapters/non-verbal/glossarist-augment.d.ts +0 -339
- /package/src/adapters/{model-bridge/jsonld-types.ts → jsonld-types.ts} +0 -0
package/cli/index.mjs
CHANGED
|
@@ -1,257 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* fetch Fetch/update datasets (from GCR packages or source repos)
|
|
8
|
-
* generate Convert harmonized YAML concepts to JSON-LD static files
|
|
9
|
-
* edges Build cross-reference edges from generated concept data
|
|
10
|
-
* build Full pipeline: fetch + generate + edges + vite build
|
|
11
|
-
* site Same as build (alias)
|
|
12
|
-
* normalize NFC-normalize YAML files in .datasets/ (use --check for CI gate)
|
|
13
|
-
* doctor Diagnose the local environment (deps, datasets, shapes, context)
|
|
14
|
-
*
|
|
15
|
-
* Options:
|
|
16
|
-
* --site <id> Site config to use (looks for site-config.yml in CWD)
|
|
17
|
-
*
|
|
18
|
-
* Environment:
|
|
19
|
-
* SITE_CONFIG Path to site config file (overrides --site)
|
|
20
|
-
* SITE_ID Site config ID (overrides --site)
|
|
21
|
-
* GITHUB_TOKEN GitHub token for private repos
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { loadSiteConfig } from '../scripts/load-site-config.mjs';
|
|
25
|
-
import { existsSync } from 'fs';
|
|
26
|
-
import { resolve, dirname } from 'path';
|
|
2
|
+
// Thin ESM shim so `concept-browser` bin works from npm consumers that
|
|
3
|
+
// don't run TypeScript natively. The real logic lives in cli/index.ts.
|
|
4
|
+
// This shim uses tsx to load the .ts entry point at runtime.
|
|
5
|
+
// When TODO.typescript/03 is complete, this shim may be replaced by a
|
|
6
|
+
// compiled cli/index.js output.
|
|
27
7
|
import { fileURLToPath } from 'url';
|
|
8
|
+
import { resolve, dirname } from 'path';
|
|
9
|
+
import { spawnSync } from 'child_process';
|
|
28
10
|
|
|
29
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const commands = {
|
|
33
|
-
fetch: async () => (await import('../scripts/fetch-datasets.mjs')).main(),
|
|
34
|
-
generate: async () => { await import('../scripts/generate-data.mjs'); },
|
|
35
|
-
edges: async () => (await import('../scripts/build-edges.js')).main(),
|
|
36
|
-
about: async () => (await import('../scripts/process-about-pages.mjs')).main(),
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function parseArgs(argv) {
|
|
40
|
-
const positional = [];
|
|
41
|
-
const named = {};
|
|
42
|
-
for (let i = 0; i < argv.length; i++) {
|
|
43
|
-
if (argv[i].startsWith('--') && i + 1 < argv.length) {
|
|
44
|
-
named[argv[i].slice(2)] = argv[i + 1];
|
|
45
|
-
i++;
|
|
46
|
-
} else if (!argv[i].startsWith('-')) {
|
|
47
|
-
positional.push(argv[i]);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return { positional, named };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function main() {
|
|
54
|
-
const args = process.argv.slice(2);
|
|
55
|
-
const { positional, named } = parseArgs(args);
|
|
56
|
-
const cmd = positional[0];
|
|
57
|
-
|
|
58
|
-
if (!cmd || cmd === 'help' || cmd === '--help') {
|
|
59
|
-
console.log(`concept-browser <command> [options]
|
|
60
|
-
|
|
61
|
-
Commands:
|
|
62
|
-
fetch Fetch/update datasets from GCR packages or source repos
|
|
63
|
-
generate Convert YAML concepts to JSON-LD static data
|
|
64
|
-
edges Build cross-reference edges from generated concepts
|
|
65
|
-
about Compile per-dataset and per-group about pages
|
|
66
|
-
build Full pipeline (fetch + generate + edges + about + vite build)
|
|
67
|
-
site Same as build
|
|
68
|
-
normalize NFC-normalize YAML files in .datasets/
|
|
69
|
-
doctor Diagnose the local environment (deps, datasets, shapes, context)
|
|
70
|
-
|
|
71
|
-
Options:
|
|
72
|
-
--site <id> Site config ID (looks for site-config.yml in CWD)
|
|
73
|
-
--check (normalize only) Non-mutating; exit 1 if any file is not NFC
|
|
74
|
-
|
|
75
|
-
Environment:
|
|
76
|
-
SITE_CONFIG Site config file path (highest priority)
|
|
77
|
-
SITE_ID Site config ID (same as --site)
|
|
78
|
-
GITHUB_TOKEN GitHub token for private repos`);
|
|
79
|
-
process.exit(cmd ? 0 : 1);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Pre-load site config so scripts can use it
|
|
83
|
-
if (!process.env.SITE_CONFIG && !process.env.SITE_ID && named.site) {
|
|
84
|
-
process.env.SITE_ID = named.site;
|
|
85
|
-
}
|
|
86
|
-
const { config } = loadSiteConfig(named.site ? [named.site] : []);
|
|
87
|
-
|
|
88
|
-
if (cmd === 'build' || cmd === 'site') {
|
|
89
|
-
if (!process.env.BASE_PATH && config?.basePath) {
|
|
90
|
-
process.env.BASE_PATH = config.basePath;
|
|
91
|
-
}
|
|
92
|
-
for (const step of ['fetch', 'generate', 'edges', 'about']) {
|
|
93
|
-
console.log(`\n=== ${step.toUpperCase()} ===\n`);
|
|
94
|
-
await commands[step]();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Generate favicons from site logo
|
|
98
|
-
const fs = await import('fs');
|
|
99
|
-
const publicDir = resolve(process.cwd(), 'public');
|
|
100
|
-
fs.mkdirSync(publicDir, { recursive: true });
|
|
101
|
-
|
|
102
|
-
const branding = config?.branding || {};
|
|
103
|
-
const faviconSrc =
|
|
104
|
-
(branding.favicon && resolve(process.cwd(), branding.favicon)) ||
|
|
105
|
-
(branding.logo?.localPath && resolve(process.cwd(), branding.logo.localPath)) ||
|
|
106
|
-
resolve(pkgRoot, 'public', 'favicon.svg');
|
|
107
|
-
|
|
108
|
-
let faviconHtml = '';
|
|
109
|
-
if (fs.existsSync(faviconSrc)) {
|
|
110
|
-
console.log(`\n=== FAVICONS ===\n`);
|
|
111
|
-
const favicons = (await import('favicons')).default;
|
|
112
|
-
const source = fs.readFileSync(faviconSrc);
|
|
113
|
-
const response = await favicons(source, {
|
|
114
|
-
appName: config?.title || 'Glossarist',
|
|
115
|
-
background: branding.primaryColor || '#2563eb',
|
|
116
|
-
theme_color: branding.primaryColor || '#2563eb',
|
|
117
|
-
icons: {
|
|
118
|
-
android: false,
|
|
119
|
-
appleIcon: true,
|
|
120
|
-
appleStartup: false,
|
|
121
|
-
favicons: true,
|
|
122
|
-
windows: false,
|
|
123
|
-
yandex: false,
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Write all generated images and files to public/
|
|
128
|
-
for (const img of response.images) {
|
|
129
|
-
fs.writeFileSync(resolve(publicDir, img.name), img.contents);
|
|
130
|
-
}
|
|
131
|
-
for (const file of response.files) {
|
|
132
|
-
fs.writeFileSync(resolve(publicDir, file.name), file.contents);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Keep the original SVG for dark mode support
|
|
136
|
-
if (faviconSrc.endsWith('.svg')) {
|
|
137
|
-
fs.copyFileSync(faviconSrc, resolve(publicDir, 'favicon.svg'));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
faviconHtml = response.html.join('\n ');
|
|
141
|
-
// Add SVG favicon
|
|
142
|
-
if (faviconSrc.endsWith('.svg')) {
|
|
143
|
-
faviconHtml += '\n <link rel="icon" type="image/svg+xml" href="/favicon.svg">';
|
|
144
|
-
}
|
|
145
|
-
// Write tags for Vite plugin to pick up
|
|
146
|
-
fs.writeFileSync(resolve(publicDir, 'favicon-links.html'), faviconHtml);
|
|
147
|
-
console.log(` Generated ${response.images.length} favicon files`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Pass favicon tags to Vite via env
|
|
151
|
-
if (faviconHtml) {
|
|
152
|
-
const basePath = process.env.BASE_PATH?.replace(/\/+$/, '') || '';
|
|
153
|
-
if (basePath) {
|
|
154
|
-
faviconHtml = faviconHtml.replace(/(href|content)="\/([^"]+)"/g, `$1="${basePath}/$2"`);
|
|
155
|
-
}
|
|
156
|
-
process.env.FAVICON_HTML = faviconHtml;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Pass branding info for HTML transformation
|
|
160
|
-
process.env.SITE_TITLE = config?.title || 'Glossarist';
|
|
161
|
-
if (branding.fonts?.header || branding.fonts?.body) {
|
|
162
|
-
const fontFamilies = [];
|
|
163
|
-
if (branding.fonts.header?.source === 'google') {
|
|
164
|
-
const w = (branding.fonts.header.weights || [400, 700]).join(';');
|
|
165
|
-
fontFamilies.push(`family=${branding.fonts.header.family.replace(/ /g, '+')}:wght@${w}`);
|
|
166
|
-
}
|
|
167
|
-
if (branding.fonts.body?.source === 'google') {
|
|
168
|
-
const w = (branding.fonts.body.weights || [400, 700]).join(';');
|
|
169
|
-
fontFamilies.push(`family=${branding.fonts.body.family.replace(/ /g, '+')}:wght@${w}`);
|
|
170
|
-
}
|
|
171
|
-
if (fontFamilies.length) {
|
|
172
|
-
process.env.SITE_FONTS_URL = `https://fonts.googleapis.com/css2?${fontFamilies.join('&')}&display=swap`;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Bridge data to Astro content collections
|
|
177
|
-
console.log(`\n=== BRIDGE DATA ===\n`);
|
|
178
|
-
const bridge = resolve(pkgRoot, 'scripts', 'bridge-to-astro.mjs');
|
|
179
|
-
if (existsSync(bridge)) {
|
|
180
|
-
await import(`file://${bridge}`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Run Astro build (TODO.astro/22)
|
|
184
|
-
console.log(`\n=== BUILD ASTRO ===\n`);
|
|
185
|
-
const astroConfig = resolve(pkgRoot, 'astro.config.mjs');
|
|
186
|
-
if (existsSync(astroConfig)) {
|
|
187
|
-
try {
|
|
188
|
-
const { build: astroBuild } = await import('astro');
|
|
189
|
-
await astroBuild({ root: pkgRoot, logLevel: 'info' });
|
|
190
|
-
} catch (e) {
|
|
191
|
-
console.warn(` Astro build failed (${e.message}), falling back to Vite SPA`);
|
|
192
|
-
console.log(`\n=== BUILD SPA ===\n`);
|
|
193
|
-
const viteConfig = resolve(pkgRoot, 'vite.config.ts');
|
|
194
|
-
const { build: viteBuild } = await import('vite');
|
|
195
|
-
await viteBuild({ configFile: viteConfig, root: pkgRoot, mode: 'production' });
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
// Fall back to Vite SPA if Astro config doesn't exist
|
|
199
|
-
console.log(`\n=== BUILD SPA ===\n`);
|
|
200
|
-
const viteConfig = resolve(pkgRoot, 'vite.config.ts');
|
|
201
|
-
const { build: viteBuild } = await import('vite');
|
|
202
|
-
await viteBuild({ configFile: viteConfig, root: pkgRoot, mode: 'production' });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Run postbuild (404 page) via dynamic import
|
|
206
|
-
const postbuild = resolve(pkgRoot, 'scripts', 'generate-404.js');
|
|
207
|
-
if (existsSync(postbuild)) {
|
|
208
|
-
await import(`file://${postbuild}`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const runner = commands[cmd];
|
|
215
|
-
if (runner) {
|
|
216
|
-
await runner();
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (cmd === 'normalize') {
|
|
221
|
-
const { normalizeYaml } = await import('../scripts/normalize-yaml.mjs');
|
|
222
|
-
const check = process.argv.includes('--check');
|
|
223
|
-
const paths = process.argv.slice(2).filter(a => !a.startsWith('-') && a !== 'normalize');
|
|
224
|
-
const { checked, nonNfc, fixed } = normalizeYaml({ check, paths });
|
|
225
|
-
if (check) {
|
|
226
|
-
if (nonNfc === 0) {
|
|
227
|
-
console.log(`NFC OK: ${checked} file(s) checked, all normalized`);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
console.error(`NFC check failed: ${nonNfc} of ${checked} file(s) are not NFC-normalized\n`);
|
|
231
|
-
for (const f of fixed) console.error(` ${f}`);
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|
|
234
|
-
if (nonNfc === 0) {
|
|
235
|
-
console.log(`NFC OK: ${checked} file(s) checked, all already normalized`);
|
|
236
|
-
} else {
|
|
237
|
-
console.log(`Normalized ${nonNfc} of ${checked} file(s)`);
|
|
238
|
-
for (const f of fixed) console.log(` ${f}`);
|
|
239
|
-
}
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (cmd === 'doctor') {
|
|
244
|
-
const { main: doctorMain } = await import('../scripts/doctor.mjs');
|
|
245
|
-
await doctorMain();
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
12
|
+
const entry = resolve(__dirname, 'index.ts');
|
|
248
13
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
process.
|
|
252
|
-
}
|
|
14
|
+
const result = spawnSync(
|
|
15
|
+
process.execPath,
|
|
16
|
+
['--import', 'tsx', entry, ...process.argv.slice(2)],
|
|
17
|
+
{ stdio: 'inherit', cwd: process.cwd() }
|
|
18
|
+
);
|
|
253
19
|
|
|
254
|
-
|
|
255
|
-
console.error(e);
|
|
256
|
-
process.exit(1);
|
|
257
|
-
});
|
|
20
|
+
process.exit(result.status ?? 1);
|
package/cli/index.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Glossarist Concept Browser CLI.
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* fetch Fetch/update datasets (from GCR packages or source repos)
|
|
8
|
+
* generate Convert harmonized YAML concepts to JSON-LD static files
|
|
9
|
+
* edges Build cross-reference edges from generated concept data
|
|
10
|
+
* build Full pipeline: fetch + generate + edges + vite build
|
|
11
|
+
* site Same as build (alias)
|
|
12
|
+
* normalize NFC-normalize YAML files in .datasets/ (use --check for CI gate)
|
|
13
|
+
* doctor Diagnose the local environment (deps, datasets, shapes, context)
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* --site <id> Site config to use (looks for site-config.yml in CWD)
|
|
17
|
+
*
|
|
18
|
+
* Environment:
|
|
19
|
+
* SITE_CONFIG Path to site config file (overrides --site)
|
|
20
|
+
* SITE_ID Site config ID (overrides --site)
|
|
21
|
+
* GITHUB_TOKEN GitHub token for private repos
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Scripts are still .mjs (TODO.typescript/02). These imports will be fully
|
|
25
|
+
// typed once the scripts convert to .ts.
|
|
26
|
+
// @ts-expect-error — .mjs module without declaration
|
|
27
|
+
import { loadSiteConfig } from '../scripts/load-site-config.mjs';
|
|
28
|
+
import { existsSync } from 'fs';
|
|
29
|
+
import { resolve, dirname } from 'path';
|
|
30
|
+
import { fileURLToPath } from 'url';
|
|
31
|
+
|
|
32
|
+
const __dirname: string = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const pkgRoot: string = resolve(__dirname, '..');
|
|
34
|
+
|
|
35
|
+
interface ParsedArgs {
|
|
36
|
+
positional: string[];
|
|
37
|
+
named: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type CommandRunner = () => Promise<void>;
|
|
41
|
+
|
|
42
|
+
const commands: Record<string, CommandRunner> = {
|
|
43
|
+
fetch: async () => (await import('../scripts/fetch-datasets.mjs' as string)).main(),
|
|
44
|
+
generate: async () => { await import('../scripts/generate-data.mjs' as string); },
|
|
45
|
+
edges: async () => (await import('../scripts/build-edges.js' as string)).main(),
|
|
46
|
+
about: async () => (await import('../scripts/process-about-pages.mjs' as string)).main(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
50
|
+
const positional: string[] = [];
|
|
51
|
+
const named: Record<string, string> = {};
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
if (argv[i].startsWith('--') && i + 1 < argv.length) {
|
|
54
|
+
named[argv[i].slice(2)] = argv[i + 1];
|
|
55
|
+
i++;
|
|
56
|
+
} else if (!argv[i].startsWith('-')) {
|
|
57
|
+
positional.push(argv[i]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { positional, named };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface BrandingFonts {
|
|
64
|
+
source?: string;
|
|
65
|
+
family?: string;
|
|
66
|
+
weights?: number[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface BrandingConfig {
|
|
70
|
+
favicon?: string;
|
|
71
|
+
logo?: { localPath?: string };
|
|
72
|
+
primaryColor?: string;
|
|
73
|
+
fonts?: { header?: BrandingFonts; body?: BrandingFonts };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface SiteConfig {
|
|
77
|
+
basePath?: string;
|
|
78
|
+
title?: string;
|
|
79
|
+
branding?: BrandingConfig;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface FaviconResponse {
|
|
83
|
+
images: Array<{ name: string; contents: string | Buffer }>;
|
|
84
|
+
files: Array<{ name: string; contents: string | Buffer }>;
|
|
85
|
+
html: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function runBuildPipeline(config: SiteConfig | null): Promise<void> {
|
|
89
|
+
if (!process.env.BASE_PATH && config?.basePath) {
|
|
90
|
+
process.env.BASE_PATH = config.basePath;
|
|
91
|
+
}
|
|
92
|
+
for (const step of ['fetch', 'generate', 'edges', 'about'] as const) {
|
|
93
|
+
console.log(`\n=== ${step.toUpperCase()} ===\n`);
|
|
94
|
+
await commands[step]();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fs = await import('fs');
|
|
98
|
+
const publicDir: string = resolve(process.cwd(), 'public');
|
|
99
|
+
fs.mkdirSync(publicDir, { recursive: true });
|
|
100
|
+
|
|
101
|
+
const branding: BrandingConfig = config?.branding ?? {};
|
|
102
|
+
const faviconSrc: string =
|
|
103
|
+
(branding.favicon && resolve(process.cwd(), branding.favicon)) ||
|
|
104
|
+
(branding.logo?.localPath && resolve(process.cwd(), branding.logo.localPath)) ||
|
|
105
|
+
resolve(pkgRoot, 'public', 'favicon.svg');
|
|
106
|
+
|
|
107
|
+
let faviconHtml = '';
|
|
108
|
+
if (fs.existsSync(faviconSrc)) {
|
|
109
|
+
console.log(`\n=== FAVICONS ===\n`);
|
|
110
|
+
const favicons = (await import('favicons')).default;
|
|
111
|
+
const source: Buffer = fs.readFileSync(faviconSrc);
|
|
112
|
+
const response: FaviconResponse = await favicons(source, {
|
|
113
|
+
appName: config?.title || 'Glossarist',
|
|
114
|
+
background: branding.primaryColor || '#2563eb',
|
|
115
|
+
theme_color: branding.primaryColor || '#2563eb',
|
|
116
|
+
icons: {
|
|
117
|
+
android: false,
|
|
118
|
+
appleIcon: true,
|
|
119
|
+
appleStartup: false,
|
|
120
|
+
favicons: true,
|
|
121
|
+
windows: false,
|
|
122
|
+
yandex: false,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
for (const img of response.images) {
|
|
127
|
+
fs.writeFileSync(resolve(publicDir, img.name), img.contents);
|
|
128
|
+
}
|
|
129
|
+
for (const file of response.files) {
|
|
130
|
+
fs.writeFileSync(resolve(publicDir, file.name), file.contents);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (faviconSrc.endsWith('.svg')) {
|
|
134
|
+
fs.copyFileSync(faviconSrc, resolve(publicDir, 'favicon.svg'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
faviconHtml = response.html.join('\n ');
|
|
138
|
+
if (faviconSrc.endsWith('.svg')) {
|
|
139
|
+
faviconHtml += '\n <link rel="icon" type="image/svg+xml" href="/favicon.svg">';
|
|
140
|
+
}
|
|
141
|
+
fs.writeFileSync(resolve(publicDir, 'favicon-links.html'), faviconHtml);
|
|
142
|
+
console.log(` Generated ${response.images.length} favicon files`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (faviconHtml) {
|
|
146
|
+
const basePath: string = process.env.BASE_PATH?.replace(/\/+$/, '') || '';
|
|
147
|
+
if (basePath) {
|
|
148
|
+
faviconHtml = faviconHtml.replace(/(href|content)="\/([^"]+)"/g, `$1="${basePath}/$2"`);
|
|
149
|
+
}
|
|
150
|
+
process.env.FAVICON_HTML = faviconHtml;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
process.env.SITE_TITLE = config?.title || 'Glossarist';
|
|
154
|
+
if (branding.fonts?.header || branding.fonts?.body) {
|
|
155
|
+
const fontFamilies: string[] = [];
|
|
156
|
+
if (branding.fonts.header?.source === 'google') {
|
|
157
|
+
const w: string = (branding.fonts.header.weights || [400, 700]).join(';');
|
|
158
|
+
fontFamilies.push(`family=${branding.fonts.header.family!.replace(/ /g, '+')}:wght@${w}`);
|
|
159
|
+
}
|
|
160
|
+
if (branding.fonts.body?.source === 'google') {
|
|
161
|
+
const w: string = (branding.fonts.body.weights || [400, 700]).join(';');
|
|
162
|
+
fontFamilies.push(`family=${branding.fonts.body.family!.replace(/ /g, '+')}:wght@${w}`);
|
|
163
|
+
}
|
|
164
|
+
if (fontFamilies.length) {
|
|
165
|
+
process.env.SITE_FONTS_URL = `https://fonts.googleapis.com/css2?${fontFamilies.join('&')}&display=swap`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(`\n=== BRIDGE DATA ===\n`);
|
|
170
|
+
const bridge: string = resolve(pkgRoot, 'scripts', 'bridge-to-astro.mjs');
|
|
171
|
+
if (existsSync(bridge)) {
|
|
172
|
+
await import(`file://${bridge}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(`\n=== BUILD ASTRO ===\n`);
|
|
176
|
+
const astroConfig: string = resolve(pkgRoot, 'astro.config.mjs');
|
|
177
|
+
if (existsSync(astroConfig)) {
|
|
178
|
+
try {
|
|
179
|
+
const { build: astroBuild } = await import('astro');
|
|
180
|
+
await astroBuild({ root: pkgRoot, logLevel: 'info' });
|
|
181
|
+
} catch (e: unknown) {
|
|
182
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
183
|
+
console.warn(` Astro build failed (${msg}), falling back to Vite SPA`);
|
|
184
|
+
console.log(`\n=== BUILD SPA ===\n`);
|
|
185
|
+
const viteConfig: string = resolve(pkgRoot, 'vite.config.ts');
|
|
186
|
+
const { build: viteBuild } = await import('vite');
|
|
187
|
+
await viteBuild({ configFile: viteConfig, root: pkgRoot, mode: 'production' });
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
console.log(`\n=== BUILD SPA ===\n`);
|
|
191
|
+
const viteConfig: string = resolve(pkgRoot, 'vite.config.ts');
|
|
192
|
+
const { build: viteBuild } = await import('vite');
|
|
193
|
+
await viteBuild({ configFile: viteConfig, root: pkgRoot, mode: 'production' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const postbuild: string = resolve(pkgRoot, 'scripts', 'generate-404.js');
|
|
197
|
+
if (existsSync(postbuild)) {
|
|
198
|
+
await import(`file://${postbuild}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function runNormalize(): Promise<void> {
|
|
203
|
+
const mod = await import('../scripts/normalize-yaml.mjs' as string);
|
|
204
|
+
const normalizeYaml = mod.normalizeYaml;
|
|
205
|
+
const check: boolean = process.argv.includes('--check');
|
|
206
|
+
const paths: string[] = process.argv.slice(2).filter(a => !a.startsWith('-') && a !== 'normalize');
|
|
207
|
+
const { checked, nonNfc, fixed }: { checked: number; nonNfc: number; fixed: string[] } = normalizeYaml({ check, paths });
|
|
208
|
+
if (check) {
|
|
209
|
+
if (nonNfc === 0) {
|
|
210
|
+
console.log(`NFC OK: ${checked} file(s) checked, all normalized`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.error(`NFC check failed: ${nonNfc} of ${checked} file(s) are not NFC-normalized\n`);
|
|
214
|
+
for (const f of fixed) console.error(` ${f}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
if (nonNfc === 0) {
|
|
218
|
+
console.log(`NFC OK: ${checked} file(s) checked, all already normalized`);
|
|
219
|
+
} else {
|
|
220
|
+
console.log(`Normalized ${nonNfc} of ${checked} file(s)`);
|
|
221
|
+
for (const f of fixed) console.log(` ${f}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function main(): Promise<void> {
|
|
226
|
+
const args: readonly string[] = process.argv.slice(2);
|
|
227
|
+
const { positional, named }: ParsedArgs = parseArgs(args);
|
|
228
|
+
const cmd: string | undefined = positional[0];
|
|
229
|
+
|
|
230
|
+
if (!cmd || cmd === 'help' || cmd === '--help') {
|
|
231
|
+
console.log(`concept-browser <command> [options]
|
|
232
|
+
|
|
233
|
+
Commands:
|
|
234
|
+
fetch Fetch/update datasets from GCR packages or source repos
|
|
235
|
+
generate Convert YAML concepts to JSON-LD static data
|
|
236
|
+
edges Build cross-reference edges from generated concepts
|
|
237
|
+
about Compile per-dataset and per-group about pages
|
|
238
|
+
build Full pipeline (fetch + generate + edges + about + vite build)
|
|
239
|
+
site Same as build
|
|
240
|
+
normalize NFC-normalize YAML files in .datasets/
|
|
241
|
+
doctor Diagnose the local environment (deps, datasets, shapes, context)
|
|
242
|
+
|
|
243
|
+
Options:
|
|
244
|
+
--site <id> Site config ID (looks for site-config.yml in CWD)
|
|
245
|
+
--check (normalize only) Non-mutating; exit 1 if any file is not NFC
|
|
246
|
+
|
|
247
|
+
Environment:
|
|
248
|
+
SITE_CONFIG Site config file path (highest priority)
|
|
249
|
+
SITE_ID Site config ID (same as --site)
|
|
250
|
+
GITHUB_TOKEN GitHub token for private repos`);
|
|
251
|
+
process.exit(cmd ? 0 : 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!process.env.SITE_CONFIG && !process.env.SITE_ID && named.site) {
|
|
255
|
+
process.env.SITE_ID = named.site;
|
|
256
|
+
}
|
|
257
|
+
const { config }: { config: SiteConfig | null } = loadSiteConfig(named.site ? [named.site] : []);
|
|
258
|
+
|
|
259
|
+
if (cmd === 'build' || cmd === 'site') {
|
|
260
|
+
await runBuildPipeline(config);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const runner: CommandRunner | undefined = commands[cmd];
|
|
265
|
+
if (runner) {
|
|
266
|
+
await runner();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (cmd === 'normalize') {
|
|
271
|
+
await runNormalize();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (cmd === 'doctor') {
|
|
276
|
+
const mod = await import('../scripts/doctor.mjs' as string);
|
|
277
|
+
await mod.main();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.error(`Unknown command: ${cmd}`);
|
|
282
|
+
console.error('Run `concept-browser help` for usage.');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
main().catch((e: unknown) => {
|
|
287
|
+
console.error(e);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.102",
|
|
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": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"astro": "^7.0.7",
|
|
38
38
|
"d3": "^7.9.0",
|
|
39
39
|
"favicons": "^7.2.0",
|
|
40
|
-
"glossarist": "^0.4.
|
|
40
|
+
"glossarist": "^0.4.51",
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"jszip": "^3.10.1",
|
|
43
43
|
"pinia": "^2.3.1",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"happy-dom": "^20.9.0",
|
|
60
60
|
"n3": "^1.26.0",
|
|
61
61
|
"rdf-validate-shacl": "^0.4.5",
|
|
62
|
+
"tsx": "^4.23.1",
|
|
62
63
|
"typescript": "~5.7.3",
|
|
63
64
|
"vitest": "^4.1.5",
|
|
64
65
|
"vue-tsc": "^2.2.8"
|
|
@@ -87,8 +87,8 @@ function withSources(): Concept {
|
|
|
87
87
|
type: 'authoritative',
|
|
88
88
|
modification: 'revised 2024',
|
|
89
89
|
origin: {
|
|
90
|
-
ref: { source: 'ISO 704', id: '3.1', version: '2020' },
|
|
91
|
-
locality: { type: 'clause', referenceFrom: '3.1', referenceTo: '3.5' },
|
|
90
|
+
ref: { source: 'ISO 704', id: '3.1', version: '2020' } as any,
|
|
91
|
+
locality: { type: 'clause', referenceFrom: '3.1', referenceTo: '3.5' } as any,
|
|
92
92
|
link: 'https://example.org/iso-704',
|
|
93
93
|
original: 'Original wording here.',
|
|
94
94
|
},
|
|
@@ -98,7 +98,7 @@ function withSources(): Concept {
|
|
|
98
98
|
type: 'lineage',
|
|
99
99
|
origin: {
|
|
100
100
|
ref: { source: 'IEC 60050', id: '102-01', version: '2008' },
|
|
101
|
-
locality: { type: 'clause', referenceFrom: '102-01-01' },
|
|
101
|
+
locality: { type: 'clause', referenceFrom: '102-01-01' } as any,
|
|
102
102
|
},
|
|
103
103
|
},
|
|
104
104
|
{
|
|
@@ -197,8 +197,8 @@ describe('designation target matching robustness', () => {
|
|
|
197
197
|
// Terms are in model order, not raw order
|
|
198
198
|
const pdf = lc.terms.find(t => t.designation === 'PDF')!;
|
|
199
199
|
const iso = lc.terms.find(t => t.designation === 'ISO')!;
|
|
200
|
-
expect(getDesignationTarget(pdf.related[0])).toBe('Portable Document Format');
|
|
201
|
-
expect(getDesignationTarget(iso.related[0])).toBe('International Organization for Standardization');
|
|
200
|
+
expect(getDesignationTarget(pdf.related[0] as any)).toBe('Portable Document Format');
|
|
201
|
+
expect(getDesignationTarget(iso.related[0] as any)).toBe('International Organization for Standardization');
|
|
202
202
|
});
|
|
203
203
|
});
|
|
204
204
|
|