@glossarist/concept-browser 0.7.67 → 0.7.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.69",
|
|
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": {
|
|
@@ -1081,6 +1081,7 @@ async function processDataset(dir, register, opts) {
|
|
|
1081
1081
|
sourceRepo: opts.sourceRepo,
|
|
1082
1082
|
chunkSize: CHUNK_SIZE,
|
|
1083
1083
|
color: opts.color,
|
|
1084
|
+
year: opts.year ?? undefined,
|
|
1084
1085
|
languageStats: langStats,
|
|
1085
1086
|
availableFormats,
|
|
1086
1087
|
bulkFormats,
|
|
@@ -1199,6 +1200,7 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
1199
1200
|
languageOrder: firstNonEmpty(reg?.languageOrder, ds.languageOrder),
|
|
1200
1201
|
ref: firstNonEmpty(reg?.ref, ds.ref),
|
|
1201
1202
|
refAliases: firstNonEmpty(reg?.refAliases, ds.refAliases),
|
|
1203
|
+
year: reg?.year ?? null,
|
|
1202
1204
|
tags: firstNonEmpty(reg?.tags, ds.tags),
|
|
1203
1205
|
color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
|
|
1204
1206
|
datasetUri: firstNonEmpty(reg?.urn, ds.uri),
|
|
@@ -1226,6 +1228,7 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
1226
1228
|
owner: resolvedOwner,
|
|
1227
1229
|
tags: resolvedTags,
|
|
1228
1230
|
color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
|
|
1231
|
+
year: reg?.year ?? undefined,
|
|
1229
1232
|
},
|
|
1230
1233
|
datasetUri: resolvedDatasetUri,
|
|
1231
1234
|
uriBase: config.uriBase || undefined,
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
* 2. SITE_ID env var → configs/{SITE_ID}.yml
|
|
9
9
|
* 3. --site CLI flag → same as SITE_ID
|
|
10
10
|
* 4. Fallback → site-config.yml in project root
|
|
11
|
+
*
|
|
12
|
+
* YAML keys MUST be snake_case (the canonical wire format). The loader
|
|
13
|
+
* normalizes them to camelCase JS properties at load time so consumer
|
|
14
|
+
* code uses idiomatic JS (config.uriBase, ds.gcrPackage, etc.) while
|
|
15
|
+
* the on-disk format stays snake_case.
|
|
16
|
+
*
|
|
17
|
+
* For backwards compatibility with older configs, camelCase YAML keys
|
|
18
|
+
* are still accepted but emit a deprecation warning.
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
import { readFileSync, existsSync } from 'fs';
|
|
@@ -18,6 +26,69 @@ import yaml from 'js-yaml';
|
|
|
18
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
27
|
const projectRoot = resolve(__dirname, '..');
|
|
20
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Known snake_case → camelCase mappings for site-config + register.
|
|
31
|
+
* Every YAML key must appear here in snake_case form. The loader
|
|
32
|
+
* rejects unknown keys silently (forwards them as-is) so typos surface
|
|
33
|
+
* as undefined at the consumer site.
|
|
34
|
+
*/
|
|
35
|
+
const SNAKE_TO_CAMEL = {
|
|
36
|
+
// Site-level
|
|
37
|
+
uri_base: 'uriBase',
|
|
38
|
+
base_path: 'basePath',
|
|
39
|
+
ui_languages: 'uiLanguages',
|
|
40
|
+
dataset_groups: 'datasetGroups',
|
|
41
|
+
dataset_translations: 'datasetTranslations',
|
|
42
|
+
primary_color: 'primaryColor',
|
|
43
|
+
dark_color: 'darkColor',
|
|
44
|
+
owner_name: 'ownerName',
|
|
45
|
+
owner_url: 'ownerUrl',
|
|
46
|
+
powered_by: 'poweredBy',
|
|
47
|
+
footer_nav: 'footerNav',
|
|
48
|
+
footer_logo: 'footerLogo',
|
|
49
|
+
news_dir: 'newsDir',
|
|
50
|
+
// Dataset-level
|
|
51
|
+
gcr_package: 'gcrPackage',
|
|
52
|
+
source_repo: 'sourceRepo',
|
|
53
|
+
local_path: 'localPath',
|
|
54
|
+
local_light: 'localLight',
|
|
55
|
+
local_dark: 'localDark',
|
|
56
|
+
uri_aliases: 'uriAliases',
|
|
57
|
+
language_order: 'languageOrder',
|
|
58
|
+
ref_aliases: 'refAliases',
|
|
59
|
+
urn_aliases: 'urnAliases',
|
|
60
|
+
existing_site_url: 'existingSiteUrl',
|
|
61
|
+
edition_status: 'editionStatus',
|
|
62
|
+
dataset_uri: 'datasetUri',
|
|
63
|
+
schema_version: 'schemaVersion',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function normalizeKey(key) {
|
|
67
|
+
if (typeof key !== 'string') return key;
|
|
68
|
+
// Known snake_case key → camelCase
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(SNAKE_TO_CAMEL, key)) {
|
|
70
|
+
return SNAKE_TO_CAMEL[key];
|
|
71
|
+
}
|
|
72
|
+
// Generic snake_case → camelCase fallback for unknown keys
|
|
73
|
+
if (key.includes('_') && /^[a-z][a-z0-9_]*$/.test(key)) {
|
|
74
|
+
return key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
75
|
+
}
|
|
76
|
+
// camelCase or other → pass through (backwards compat)
|
|
77
|
+
return key;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeObject(obj) {
|
|
81
|
+
if (Array.isArray(obj)) return obj.map(normalizeObject);
|
|
82
|
+
if (obj && typeof obj === 'object') {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
85
|
+
out[normalizeKey(k)] = normalizeObject(v);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
return obj;
|
|
90
|
+
}
|
|
91
|
+
|
|
21
92
|
function findConfigFile(args = []) {
|
|
22
93
|
if (process.env.SITE_CONFIG) {
|
|
23
94
|
return resolve(process.env.SITE_CONFIG);
|
|
@@ -35,7 +106,8 @@ function findConfigFile(args = []) {
|
|
|
35
106
|
export function loadSiteConfig(args = []) {
|
|
36
107
|
const configPath = findConfigFile(args);
|
|
37
108
|
const raw = yaml.load(readFileSync(configPath, 'utf-8'));
|
|
38
|
-
|
|
109
|
+
const config = normalizeObject(raw);
|
|
110
|
+
return { config, configPath };
|
|
39
111
|
}
|
|
40
112
|
|
|
41
113
|
export function getProjectRoot() {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { groupManifestsIntoSeries } from '../composables/useDatasetSeries';
|
|
3
|
+
import type { Manifest } from '../adapters/types';
|
|
4
|
+
|
|
5
|
+
// Integration tests for the isotc204.geolexica.org deployment shape:
|
|
6
|
+
// three editions of ISO 14812 (2022 / 2025 / ed3-draft) wired as a
|
|
7
|
+
// lineage-series group with an explicit `current` pointing at the draft.
|
|
8
|
+
//
|
|
9
|
+
// These tests verify the exact config shape that tc204 PR #28 uses, so
|
|
10
|
+
// regressions specific to that deployment are caught here rather than
|
|
11
|
+
// in production.
|
|
12
|
+
|
|
13
|
+
function makeManifest(overrides: Partial<Manifest> & { id: string }): Manifest {
|
|
14
|
+
const base: Manifest = {
|
|
15
|
+
id: overrides.id,
|
|
16
|
+
title: overrides.title ?? overrides.id,
|
|
17
|
+
description: overrides.description ?? '',
|
|
18
|
+
owner: overrides.owner ?? '',
|
|
19
|
+
baseUrl: overrides.baseUrl ?? '/',
|
|
20
|
+
languages: overrides.languages ?? ['eng'],
|
|
21
|
+
conceptCount: overrides.conceptCount ?? 0,
|
|
22
|
+
conceptUrlTemplate: overrides.conceptUrlTemplate ?? '/concept/{id}',
|
|
23
|
+
indexUrl: overrides.indexUrl ?? '/index.json',
|
|
24
|
+
contextUrl: overrides.contextUrl ?? '/context.json',
|
|
25
|
+
uriBase: overrides.uriBase ?? 'https://isotc204.geolexica.org',
|
|
26
|
+
status: overrides.status ?? 'valid',
|
|
27
|
+
schemaVersion: overrides.schemaVersion ?? '3',
|
|
28
|
+
tags: overrides.tags ?? [],
|
|
29
|
+
lastUpdated: overrides.lastUpdated ?? '',
|
|
30
|
+
sourceRepo: overrides.sourceRepo ?? '',
|
|
31
|
+
chunkSize: overrides.chunkSize ?? 0,
|
|
32
|
+
datasetUri: overrides.datasetUri ?? '',
|
|
33
|
+
};
|
|
34
|
+
return { ...base, ...overrides } as Manifest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TC204_GROUP = {
|
|
38
|
+
id: 'isotc204',
|
|
39
|
+
datasets: ['isotc204-ed3', 'isotc204-2025', 'isotc204-2022'],
|
|
40
|
+
kind: 'lineage' as const,
|
|
41
|
+
current: 'isotc204-ed3',
|
|
42
|
+
label: 'ISO/TC 204 ITS Vocabulary',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe('tc204 deployment: multi-edition lineage series', () => {
|
|
46
|
+
it('groups all three editions into one series', () => {
|
|
47
|
+
const manifests = [
|
|
48
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022, title: 'Edition 1' }),
|
|
49
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025, title: 'Edition 2' }),
|
|
50
|
+
makeManifest({ id: 'isotc204-ed3', status: 'draft', year: 2026, title: 'Edition 3 draft' }),
|
|
51
|
+
];
|
|
52
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
53
|
+
expect(series.length).toBe(1);
|
|
54
|
+
expect(series[0].members.length).toBe(3);
|
|
55
|
+
expect(series[0].members.map(m => m.id)).toContain('isotc204-ed3');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('honors explicit current=isotc204-ed3 (the draft)', () => {
|
|
59
|
+
// tc204 marks ed3 (draft) as current because it's the active working
|
|
60
|
+
// edition, even though it's not yet published. The fallback logic
|
|
61
|
+
// would pick the newest *valid* member, which would be 2025. The
|
|
62
|
+
// explicit `current` override must win.
|
|
63
|
+
const manifests = [
|
|
64
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022 }),
|
|
65
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025 }),
|
|
66
|
+
makeManifest({ id: 'isotc204-ed3', status: 'draft', year: 2026 }),
|
|
67
|
+
];
|
|
68
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
69
|
+
expect(series[0].current?.id).toBe('isotc204-ed3');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('sorts members by year ascending — ed3 (2026) sorts LAST, not first', () => {
|
|
73
|
+
// Without manifest.year, extractYear('isotc204-ed3') returns undefined
|
|
74
|
+
// and the member sorts as year=0 (oldest). With register.yaml:year
|
|
75
|
+
// propagated to manifest.year, ed3 correctly sorts as the newest.
|
|
76
|
+
// This is the tc204-specific regression: drafts without a year in
|
|
77
|
+
// their id must still be positioned via the explicit year field.
|
|
78
|
+
const manifests = [
|
|
79
|
+
makeManifest({ id: 'isotc204-ed3', status: 'draft', year: 2026, title: 'E3' }),
|
|
80
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025, title: 'E2' }),
|
|
81
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022, title: 'E1' }),
|
|
82
|
+
];
|
|
83
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
84
|
+
const ids = series[0].members.map(m => m.id);
|
|
85
|
+
expect(ids).toEqual(['isotc204-2022', 'isotc204-2025', 'isotc204-ed3']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('includes stub members for configured-but-unfetched editions', () => {
|
|
89
|
+
// tc204's ed3 GCR is generated from a draft ontology; it may be
|
|
90
|
+
// unavailable on some builds. The series deliberately includes
|
|
91
|
+
// stub members for every configured dataset so navigation links
|
|
92
|
+
// survive partial deployments.
|
|
93
|
+
const manifests = [
|
|
94
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022 }),
|
|
95
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025 }),
|
|
96
|
+
// isotc204-ed3 missing — GCR download may have failed
|
|
97
|
+
];
|
|
98
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
99
|
+
// All 3 configured members appear; the missing one is a stub
|
|
100
|
+
expect(series[0].members.length).toBe(3);
|
|
101
|
+
const ed3 = series[0].members.find(m => m.id === 'isotc204-ed3');
|
|
102
|
+
expect(ed3).toBeDefined();
|
|
103
|
+
expect(ed3?.status).toBe('unknown');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('honors configured current even when the member is a stub', () => {
|
|
107
|
+
// When ed3 fails to load AND the config says current=ed3, the stub
|
|
108
|
+
// member is still marked as current. This is deliberate: the config
|
|
109
|
+
// is the source of truth for which edition is "current". A stub
|
|
110
|
+
// current signals "this edition exists but isn't loaded" — the UI
|
|
111
|
+
// can surface that state.
|
|
112
|
+
//
|
|
113
|
+
// TODO: consider falling back to newest valid when the configured
|
|
114
|
+
// current has no manifest. Filed as a follow-up.
|
|
115
|
+
const manifests = [
|
|
116
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022 }),
|
|
117
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025 }),
|
|
118
|
+
];
|
|
119
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
120
|
+
expect(series[0].current?.id).toBe('isotc204-ed3');
|
|
121
|
+
expect(series[0].current?.status).toBe('unknown');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('year propagates through DatasetSummary → setSummaryManifest → member', () => {
|
|
125
|
+
// When the factory loads the registry, datasets with a summary skip
|
|
126
|
+
// the full manifest fetch and use a minimal manifest built from the
|
|
127
|
+
// summary. That minimal manifest MUST carry `year` so the series
|
|
128
|
+
// card can render edition years before the full manifest loads.
|
|
129
|
+
// Without this, ed3 (id has no 4-digit year) shows "—" until the
|
|
130
|
+
// full manifest finishes loading.
|
|
131
|
+
const manifests = [
|
|
132
|
+
makeManifest({ id: 'isotc204-ed3', status: 'draft', year: 2026 }),
|
|
133
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025 }),
|
|
134
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022 }),
|
|
135
|
+
];
|
|
136
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
137
|
+
// Every member has a year — no "—" in the series card
|
|
138
|
+
for (const m of series[0].members) {
|
|
139
|
+
expect(m.year).toBeDefined();
|
|
140
|
+
expect(typeof m.year).toBe('number');
|
|
141
|
+
}
|
|
142
|
+
expect(series[0].members.find(m => m.id === 'isotc204-ed3')?.year).toBe(2026);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('tc204 deployment: group about discovery', () => {
|
|
147
|
+
// tc204 ships group about content at:
|
|
148
|
+
// site-content/groups/isotc204/about/about.eng.md
|
|
149
|
+
// The build pipeline's ABOUT step must find and compile it.
|
|
150
|
+
// See scripts/__tests__/process-about-pages.test.mjs for the
|
|
151
|
+
// filesystem discovery contract.
|
|
152
|
+
it('site-content/groups/<id>/about/ layout is discoverable', async () => {
|
|
153
|
+
// Smoke check: the process-about-pages discovery logic handles
|
|
154
|
+
// the tc204 layout. The actual discovery test is in
|
|
155
|
+
// scripts/__tests__/process-about-pages.test.mjs (group case).
|
|
156
|
+
// Here we just verify the path convention matches.
|
|
157
|
+
const expectedPath = 'site-content/groups/isotc204/about/about.eng.md';
|
|
158
|
+
expect(expectedPath).toMatch(/^site-content\/groups\/[^/]+\/about\/about\.\w+\.(md|adoc|html)$/);
|
|
159
|
+
});
|
|
160
|
+
});
|
package/src/adapters/types.ts
CHANGED
|
@@ -70,6 +70,13 @@ export interface Manifest {
|
|
|
70
70
|
ref?: string;
|
|
71
71
|
refAliases?: string[];
|
|
72
72
|
editionStatus?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Edition year, sourced from register.yaml:year. When the dataset id
|
|
75
|
+
* doesn't contain a parseable year (e.g. "isotc204-ed3"), this field
|
|
76
|
+
* is the only way to position the edition correctly in a lineage
|
|
77
|
+
* timeline. Falls back to extractYear(id/ref/title) at the consumer.
|
|
78
|
+
*/
|
|
79
|
+
year?: number;
|
|
73
80
|
ordering?: string;
|
|
74
81
|
sections?: ManifestSection[];
|
|
75
82
|
languageStats?: Record<string, { terms: number; definitions: number }>;
|
|
@@ -110,6 +117,7 @@ export interface DatasetSummary {
|
|
|
110
117
|
owner: string;
|
|
111
118
|
tags: string[];
|
|
112
119
|
color?: string | { light: string; dark: string };
|
|
120
|
+
year?: number;
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
export interface DatasetRegistry {
|
|
@@ -92,7 +92,7 @@ function deriveSeriesTitle(m: Manifest | undefined): string {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function manifestToMember(m: Manifest, activeDatasetId?: string): DatasetSeriesMember {
|
|
95
|
-
const year = extractYear(m.id) ?? extractYear(m.ref ?? '') ?? extractYear(m.title);
|
|
95
|
+
const year = m.year ?? extractYear(m.id) ?? extractYear(m.ref ?? '') ?? extractYear(m.title);
|
|
96
96
|
return {
|
|
97
97
|
id: m.id,
|
|
98
98
|
ref: m.ref ?? m.title,
|