@glossarist/concept-browser 0.7.137 → 0.7.139
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 +2 -2
- package/scripts/__tests__/concept-uri.test.ts +45 -0
- package/scripts/build-edges.js +20 -3
- package/scripts/build-edges.ts +1 -6
- package/scripts/generate-data.js +11 -2
- package/scripts/generate-data.ts +10 -2
- package/scripts/lib/concept-uri.ts +18 -0
- package/src/__fixtures__/isotc211-domain-slugs.json +201 -0
- package/src/__tests__/dataset-adapter.test.ts +1 -1
- package/src/__tests__/scripts/concept-csv.test.ts +15 -0
- package/src/__tests__/slugify.test.ts +74 -1
- package/src/utils/slugify.ts +25 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.139",
|
|
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": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"astro": "^7.0.7",
|
|
44
44
|
"d3": "^7.9.0",
|
|
45
45
|
"favicons": "^7.2.0",
|
|
46
|
-
"glossarist": "^0.4.
|
|
46
|
+
"glossarist": "^0.4.61",
|
|
47
47
|
"js-yaml": "^4.1.0",
|
|
48
48
|
"jszip": "^3.10.1",
|
|
49
49
|
"n3": "^1.26.0",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildConceptUri,
|
|
4
|
+
buildConceptUriPrefix,
|
|
5
|
+
buildDatasetUriPrefix,
|
|
6
|
+
buildSiteOrigin,
|
|
7
|
+
} from '../lib/concept-uri';
|
|
8
|
+
|
|
9
|
+
describe('buildSiteOrigin — deployment hosting coordinate', () => {
|
|
10
|
+
it('prepends https:// to a scheme-less domain that carries a custom path', () => {
|
|
11
|
+
expect(buildSiteOrigin('oimlsmart.github.io/vocab', 'https://identity.example/viml'))
|
|
12
|
+
.toBe('https://oimlsmart.github.io/vocab');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('keeps the configured path segment (/iala-vocab)', () => {
|
|
16
|
+
expect(buildSiteOrigin('www.glossarist.org/iala-vocab', 'https://identity.example/iala'))
|
|
17
|
+
.toBe('https://www.glossarist.org/iala-vocab');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('handles an apex domain with no path', () => {
|
|
21
|
+
expect(buildSiteOrigin('isotc204.geolexica.org', 'https://isotc204.geolexica.org'))
|
|
22
|
+
.toBe('https://isotc204.geolexica.org');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('normalizes a domain that already declares a scheme and trailing slash', () => {
|
|
26
|
+
expect(buildSiteOrigin('https://www.example.org/site/', 'https://identity.example'))
|
|
27
|
+
.toBe('https://www.example.org/site');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('falls back to uriBase only when no domain is declared', () => {
|
|
31
|
+
expect(buildSiteOrigin(undefined, 'https://identity.example/reg')).toBe('https://identity.example/reg');
|
|
32
|
+
expect(buildSiteOrigin(null, 'https://identity.example/reg')).toBe('https://identity.example/reg');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('keeps identity (uriBase) and hosting (domain) independent — /dataset/ routes build on either', () => {
|
|
36
|
+
const sitePrefix = buildConceptUriPrefix(buildSiteOrigin('oimlsmart.github.io/vocab', 'https://identity.example/viml'), 'viml-2013');
|
|
37
|
+
expect(sitePrefix).toBe('https://oimlsmart.github.io/vocab/dataset/viml-2013/concept/');
|
|
38
|
+
const identityPrefix = buildConceptUriPrefix('https://identity.example/viml', 'viml-2013');
|
|
39
|
+
expect(identityPrefix).toBe('https://identity.example/viml/dataset/viml-2013/concept/');
|
|
40
|
+
expect(buildConceptUri('https://identity.example/viml', 'viml-2013', '0.01'))
|
|
41
|
+
.toBe('https://identity.example/viml/dataset/viml-2013/concept/0.01');
|
|
42
|
+
expect(buildDatasetUriPrefix('https://identity.example/viml', 'viml-2013'))
|
|
43
|
+
.toBe('https://identity.example/viml/dataset/viml-2013/');
|
|
44
|
+
});
|
|
45
|
+
});
|
package/scripts/build-edges.js
CHANGED
|
@@ -29,6 +29,26 @@ function extractSourceRefs(concept, registerId) {
|
|
|
29
29
|
import { readFileSync, writeFileSync, readdirSync, existsSync, realpathSync } from "fs";
|
|
30
30
|
import { join, dirname } from "path";
|
|
31
31
|
import { fileURLToPath } from "url";
|
|
32
|
+
|
|
33
|
+
// src/utils/slugify.ts
|
|
34
|
+
var KEEP = /[^\p{L}\p{N}\p{M}_\s-]/gu;
|
|
35
|
+
var HAS_WORD = /[\p{L}\p{N}]/u;
|
|
36
|
+
function shortHash(text) {
|
|
37
|
+
let h = 2166136261;
|
|
38
|
+
for (let i = 0; i < text.length; i++) {
|
|
39
|
+
h ^= text.charCodeAt(i);
|
|
40
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
41
|
+
}
|
|
42
|
+
return h.toString(16).padStart(8, "0");
|
|
43
|
+
}
|
|
44
|
+
function slugify(text) {
|
|
45
|
+
if (text === "") return "";
|
|
46
|
+
const folded = text.normalize("NFKC").toLowerCase();
|
|
47
|
+
const slug = folded.replace(KEEP, "").replace(/[\s/]+/g, "-");
|
|
48
|
+
return HAS_WORD.test(slug) ? slug : `x-${shortHash(folded.trim())}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// scripts/build-edges.ts
|
|
32
52
|
var VALID_COMPLETENESS = /* @__PURE__ */ new Set(["complete", "partial"]);
|
|
33
53
|
function validateCompleteness(value) {
|
|
34
54
|
if (value == null) return "complete";
|
|
@@ -82,9 +102,6 @@ function normalizeLocalizedString(content) {
|
|
|
82
102
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
83
103
|
var ROOT = process.cwd();
|
|
84
104
|
var DATA_DIR = join(ROOT, "public", "data");
|
|
85
|
-
function slugify(text) {
|
|
86
|
-
return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/[\s/]+/g, "-");
|
|
87
|
-
}
|
|
88
105
|
function extractReferences(concept, registerId) {
|
|
89
106
|
const edges = [];
|
|
90
107
|
const sourceUri = concept["@id"];
|
package/scripts/build-edges.ts
CHANGED
|
@@ -82,17 +82,12 @@ import { join, dirname, resolve } from 'path';
|
|
|
82
82
|
import { fileURLToPath } from 'url';
|
|
83
83
|
import yaml from 'js-yaml';
|
|
84
84
|
import { Register } from 'glossarist';
|
|
85
|
+
import { slugify } from '../src/utils/slugify';
|
|
85
86
|
|
|
86
87
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
87
88
|
const ROOT = process.cwd();
|
|
88
89
|
const DATA_DIR = join(ROOT, 'public', 'data');
|
|
89
90
|
|
|
90
|
-
// --- Normalization ---
|
|
91
|
-
|
|
92
|
-
function slugify(text) {
|
|
93
|
-
return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
91
|
// --- Extractors (open/closed: add new extractors to EXTRACTORS array) ---
|
|
97
92
|
|
|
98
93
|
function extractReferences(concept: Record<string, any>, registerId: string) {
|
package/scripts/generate-data.js
CHANGED
|
@@ -558,6 +558,13 @@ function buildConceptUri(uriBase, registerId, conceptId) {
|
|
|
558
558
|
function buildConceptUriPrefix(uriBase, registerId) {
|
|
559
559
|
return `${uriBase}/dataset/${registerId}/concept/`;
|
|
560
560
|
}
|
|
561
|
+
function buildSiteOrigin(domain, uriBase) {
|
|
562
|
+
if (domain) {
|
|
563
|
+
const trimmed = String(domain).trim().replace(/\/+$/, "");
|
|
564
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
565
|
+
}
|
|
566
|
+
return uriBase.replace(/\/+$/, "");
|
|
567
|
+
}
|
|
561
568
|
|
|
562
569
|
// scripts/lib/harmonize-concept.ts
|
|
563
570
|
import fs from "fs";
|
|
@@ -931,7 +938,8 @@ function buildRefMaps(config2, registerCache2) {
|
|
|
931
938
|
if (xref.refPrefixMap) Object.assign(refPrefixMap, xref.refPrefixMap);
|
|
932
939
|
const uriBase = config2.uriBase;
|
|
933
940
|
if (!uriBase) throw new Error("site-config.yml: uriBase is required");
|
|
934
|
-
|
|
941
|
+
const siteOrigin = buildSiteOrigin(config2.domain, uriBase);
|
|
942
|
+
return { patternIndex, refPrefixMap, uriBase, siteOrigin, register: null };
|
|
935
943
|
}
|
|
936
944
|
function resolveIevRef(display, term, refPrefixMap, uriBase) {
|
|
937
945
|
if (!display.startsWith("IEV:")) return null;
|
|
@@ -1374,7 +1382,8 @@ async function processDataset(dir, register, opts) {
|
|
|
1374
1382
|
const aggregate = await emitOutputSet(conceptInstances, {
|
|
1375
1383
|
registerId: register,
|
|
1376
1384
|
uriBase: refMaps.uriBase,
|
|
1377
|
-
languageOrder: opts.languageOrder || opts.languages
|
|
1385
|
+
languageOrder: opts.languageOrder || opts.languages,
|
|
1386
|
+
conceptUriPrefix: buildConceptUriPrefix(refMaps.siteOrigin, register)
|
|
1378
1387
|
});
|
|
1379
1388
|
fs2.writeFileSync(path3.join(DATA, register, `${register}.csv`), aggregate.csv);
|
|
1380
1389
|
fs2.writeFileSync(path3.join(DATA, register, `${register}.jsonld`), aggregate.jsonld);
|
package/scripts/generate-data.ts
CHANGED
|
@@ -35,7 +35,7 @@ import type {
|
|
|
35
35
|
YamlNewsFrontmatter,
|
|
36
36
|
YamlContentPage,
|
|
37
37
|
} from './lib/yaml-types';
|
|
38
|
-
import { buildConceptUri, buildConceptUriPrefix } from './lib/concept-uri';
|
|
38
|
+
import { buildConceptUri, buildConceptUriPrefix, buildSiteOrigin } from './lib/concept-uri';
|
|
39
39
|
import {
|
|
40
40
|
loadConceptFile,
|
|
41
41
|
readYaml,
|
|
@@ -246,7 +246,11 @@ function buildRefMaps(config, registerCache) {
|
|
|
246
246
|
|
|
247
247
|
const uriBase = config.uriBase;
|
|
248
248
|
if (!uriBase) throw new Error('site-config.yml: uriBase is required');
|
|
249
|
-
|
|
249
|
+
// Public hosting coordinate from the deployment's own configuration —
|
|
250
|
+
// `domain` carries any custom path (/vocab/, /iala-vocab). Used for
|
|
251
|
+
// human-facing links (CSV uri column); uriBase stays the RDF identity.
|
|
252
|
+
const siteOrigin = buildSiteOrigin(config.domain, uriBase);
|
|
253
|
+
return { patternIndex, refPrefixMap, uriBase, siteOrigin, register: null };
|
|
250
254
|
}
|
|
251
255
|
|
|
252
256
|
|
|
@@ -857,10 +861,14 @@ async function processDataset(dir, register, opts) {
|
|
|
857
861
|
|
|
858
862
|
// Aggregate distributions: single-file exports of the whole register,
|
|
859
863
|
// emitted by glossarist's output-set API from the canonical model walk.
|
|
864
|
+
// The CSV uri column is a link a human opens — it carries the SPA route
|
|
865
|
+
// shape on the deployment's configured host (siteOrigin), not the
|
|
866
|
+
// RDF-canonical IRI.
|
|
860
867
|
const aggregate = await emitOutputSet(conceptInstances, {
|
|
861
868
|
registerId: register,
|
|
862
869
|
uriBase: refMaps.uriBase,
|
|
863
870
|
languageOrder: opts.languageOrder || opts.languages,
|
|
871
|
+
conceptUriPrefix: buildConceptUriPrefix(refMaps.siteOrigin, register),
|
|
864
872
|
});
|
|
865
873
|
fs.writeFileSync(path.join(DATA, register, `${register}.csv`), aggregate.csv!);
|
|
866
874
|
fs.writeFileSync(path.join(DATA, register, `${register}.jsonld`), aggregate.jsonld!);
|
|
@@ -26,3 +26,21 @@ export function buildConceptUriPrefix(uriBase: string, registerId: string): stri
|
|
|
26
26
|
export function buildDatasetUriPrefix(uriBase: string, registerId: string): string {
|
|
27
27
|
return `${uriBase}/dataset/${registerId}/`;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Public site origin from the deployment's own configuration.
|
|
32
|
+
*
|
|
33
|
+
* site-config `domain` is the hosting coordinate and carries any custom
|
|
34
|
+
* path (e.g. "oimlsmart.github.io/vocab", "www.glossarist.org/iala-vocab",
|
|
35
|
+
* "isotc204.geolexica.org"). Links a human opens (CSV `uri` column) must
|
|
36
|
+
* use this — NOT uriBase, which is the RDF identity root and may differ
|
|
37
|
+
* from the deployed host. Falls back to uriBase only when a deployment
|
|
38
|
+
* declares no domain.
|
|
39
|
+
*/
|
|
40
|
+
export function buildSiteOrigin(domain: string | undefined | null, uriBase: string): string {
|
|
41
|
+
if (domain) {
|
|
42
|
+
const trimmed = String(domain).trim().replace(/\/+$/, '');
|
|
43
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
44
|
+
}
|
|
45
|
+
return uriBase.replace(/\/+$/, '');
|
|
46
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
{
|
|
2
|
+
"DGGS": "dggs",
|
|
3
|
+
"Euclidean geometry": "euclidean-geometry",
|
|
4
|
+
"GML": "gml",
|
|
5
|
+
"GeoDRM": "geodrm",
|
|
6
|
+
"Geometrie": "geometrie",
|
|
7
|
+
"Georeferenzierung": "georeferenzierung",
|
|
8
|
+
"ICT": "ict",
|
|
9
|
+
"Koordinate": "koordinate",
|
|
10
|
+
"LIDAR": "lidar",
|
|
11
|
+
"OAIS": "oais",
|
|
12
|
+
"OWL": "owl",
|
|
13
|
+
"RDF": "rdf",
|
|
14
|
+
"Radar": "radar",
|
|
15
|
+
"SAR": "sar",
|
|
16
|
+
"SONAR": "sonar",
|
|
17
|
+
"UML": "uml",
|
|
18
|
+
"Verweisung": "verweisung",
|
|
19
|
+
"XML": "xml",
|
|
20
|
+
"XML Schema": "xml-schema",
|
|
21
|
+
"XMLSchema": "xmlschema",
|
|
22
|
+
"addressing": "addressing",
|
|
23
|
+
"allgemein": "allgemein",
|
|
24
|
+
"as a modifier": "as-a-modifier",
|
|
25
|
+
"cobertura": "cobertura",
|
|
26
|
+
"coordenadas": "coordenadas",
|
|
27
|
+
"coordinate": "coordinate",
|
|
28
|
+
"coordinate geometry": "coordinate-geometry",
|
|
29
|
+
"couverture": "couverture",
|
|
30
|
+
"coverage": "coverage",
|
|
31
|
+
"de un sistema": "de-un-sistema",
|
|
32
|
+
"differential geometry": "differential-geometry",
|
|
33
|
+
"differential geometry, calculus": "differential-geometry-calculus",
|
|
34
|
+
"differential geometry, geodesy": "differential-geometry-geodesy",
|
|
35
|
+
"earth observation": "earth-observation",
|
|
36
|
+
"einparametrige Menge von Geometrien": "einparametrige-menge-von-geometrien",
|
|
37
|
+
"general": "general",
|
|
38
|
+
"general vocabulary": "general-vocabulary",
|
|
39
|
+
"geodesia": "geodesia",
|
|
40
|
+
"geodesy": "geodesy",
|
|
41
|
+
"geometri": "geometri",
|
|
42
|
+
"geometria": "geometria",
|
|
43
|
+
"geometry": "geometry",
|
|
44
|
+
"geometry, metric spaces": "geometry-metric-spaces",
|
|
45
|
+
"geometry, navigation": "geometry-navigation",
|
|
46
|
+
"geometry, topology": "geometry-topology",
|
|
47
|
+
"geoposicionamiento": "geoposicionamiento",
|
|
48
|
+
"geopositioning": "geopositioning",
|
|
49
|
+
"iso-10241-1": "iso-10241-1",
|
|
50
|
+
"iso-10303-227": "iso-10303-227",
|
|
51
|
+
"iso-1087-1": "iso-1087-1",
|
|
52
|
+
"iso-12005": "iso-12005",
|
|
53
|
+
"iso-12620": "iso-12620",
|
|
54
|
+
"iso-12718": "iso-12718",
|
|
55
|
+
"iso-14055-1": "iso-14055-1",
|
|
56
|
+
"iso-14721": "iso-14721",
|
|
57
|
+
"iso-17123-1": "iso-17123-1",
|
|
58
|
+
"iso-19101": "iso-19101",
|
|
59
|
+
"iso-19101-1": "iso-19101-1",
|
|
60
|
+
"iso-19101-2": "iso-19101-2",
|
|
61
|
+
"iso-19103": "iso-19103",
|
|
62
|
+
"iso-19104": "iso-19104",
|
|
63
|
+
"iso-19105": "iso-19105",
|
|
64
|
+
"iso-19106": "iso-19106",
|
|
65
|
+
"iso-19107": "iso-19107",
|
|
66
|
+
"iso-19108": "iso-19108",
|
|
67
|
+
"iso-19109": "iso-19109",
|
|
68
|
+
"iso-19110": "iso-19110",
|
|
69
|
+
"iso-19111": "iso-19111",
|
|
70
|
+
"iso-19111-2": "iso-19111-2",
|
|
71
|
+
"iso-19112": "iso-19112",
|
|
72
|
+
"iso-19113": "iso-19113",
|
|
73
|
+
"iso-19114": "iso-19114",
|
|
74
|
+
"iso-19115": "iso-19115",
|
|
75
|
+
"iso-19115-1": "iso-19115-1",
|
|
76
|
+
"iso-19115-2": "iso-19115-2",
|
|
77
|
+
"iso-19115-3": "iso-19115-3",
|
|
78
|
+
"iso-19116": "iso-19116",
|
|
79
|
+
"iso-19117": "iso-19117",
|
|
80
|
+
"iso-19118": "iso-19118",
|
|
81
|
+
"iso-19119": "iso-19119",
|
|
82
|
+
"iso-19123": "iso-19123",
|
|
83
|
+
"iso-19123-1": "iso-19123-1",
|
|
84
|
+
"iso-19123-2": "iso-19123-2",
|
|
85
|
+
"iso-19123-3": "iso-19123-3",
|
|
86
|
+
"iso-19125-1": "iso-19125-1",
|
|
87
|
+
"iso-19125-2": "iso-19125-2",
|
|
88
|
+
"iso-19126": "iso-19126",
|
|
89
|
+
"iso-19128": "iso-19128",
|
|
90
|
+
"iso-19130-1": "iso-19130-1",
|
|
91
|
+
"iso-19131": "iso-19131",
|
|
92
|
+
"iso-19132": "iso-19132",
|
|
93
|
+
"iso-19133": "iso-19133",
|
|
94
|
+
"iso-19134": "iso-19134",
|
|
95
|
+
"iso-19135": "iso-19135",
|
|
96
|
+
"iso-19135-1": "iso-19135-1",
|
|
97
|
+
"iso-19136": "iso-19136",
|
|
98
|
+
"iso-19136-1": "iso-19136-1",
|
|
99
|
+
"iso-19136-2": "iso-19136-2",
|
|
100
|
+
"iso-19141": "iso-19141",
|
|
101
|
+
"iso-19142": "iso-19142",
|
|
102
|
+
"iso-19143": "iso-19143",
|
|
103
|
+
"iso-19144-1": "iso-19144-1",
|
|
104
|
+
"iso-19144-2": "iso-19144-2",
|
|
105
|
+
"iso-19145": "iso-19145",
|
|
106
|
+
"iso-19146": "iso-19146",
|
|
107
|
+
"iso-19147": "iso-19147",
|
|
108
|
+
"iso-19148": "iso-19148",
|
|
109
|
+
"iso-19150-2": "iso-19150-2",
|
|
110
|
+
"iso-19150-4": "iso-19150-4",
|
|
111
|
+
"iso-19152": "iso-19152",
|
|
112
|
+
"iso-19153": "iso-19153",
|
|
113
|
+
"iso-19154": "iso-19154",
|
|
114
|
+
"iso-19155": "iso-19155",
|
|
115
|
+
"iso-19155-2": "iso-19155-2",
|
|
116
|
+
"iso-19156": "iso-19156",
|
|
117
|
+
"iso-19157": "iso-19157",
|
|
118
|
+
"iso-19157-1": "iso-19157-1",
|
|
119
|
+
"iso-19160-1": "iso-19160-1",
|
|
120
|
+
"iso-19160-2": "iso-19160-2",
|
|
121
|
+
"iso-19160-4": "iso-19160-4",
|
|
122
|
+
"iso-19162": "iso-19162",
|
|
123
|
+
"iso-19165-2": "iso-19165-2",
|
|
124
|
+
"iso-19168-1": "iso-19168-1",
|
|
125
|
+
"iso-19168-2": "iso-19168-2",
|
|
126
|
+
"iso-19170-1": "iso-19170-1",
|
|
127
|
+
"iso-21000-5": "iso-21000-5",
|
|
128
|
+
"iso-22028-1": "iso-22028-1",
|
|
129
|
+
"iso-22300": "iso-22300",
|
|
130
|
+
"iso-24619": "iso-24619",
|
|
131
|
+
"iso-2859-1": "iso-2859-1",
|
|
132
|
+
"iso-2859-5": "iso-2859-5",
|
|
133
|
+
"iso-31-2": "iso-31-2",
|
|
134
|
+
"iso-3534-1": "iso-3534-1",
|
|
135
|
+
"iso-3534-2": "iso-3534-2",
|
|
136
|
+
"iso-5127": "iso-5127",
|
|
137
|
+
"iso-6709": "iso-6709",
|
|
138
|
+
"iso-8000-2": "iso-8000-2",
|
|
139
|
+
"iso-80000-7": "iso-80000-7",
|
|
140
|
+
"iso-860": "iso-860",
|
|
141
|
+
"iso-8601": "iso-8601",
|
|
142
|
+
"iso-8601-1": "iso-8601-1",
|
|
143
|
+
"iso-9000": "iso-9000",
|
|
144
|
+
"iso-9241-5": "iso-9241-5",
|
|
145
|
+
"iso-dis-19123-1": "iso-dis-19123-1",
|
|
146
|
+
"iso-iec-10746-2": "iso-iec-10746-2",
|
|
147
|
+
"iso-iec-10746-3": "iso-iec-10746-3",
|
|
148
|
+
"iso-iec-18477-4": "iso-iec-18477-4",
|
|
149
|
+
"iso-iec-19501": "iso-iec-19501",
|
|
150
|
+
"iso-iec-2382": "iso-iec-2382",
|
|
151
|
+
"iso-iec-2382-1": "iso-iec-2382-1",
|
|
152
|
+
"iso-iec-2382-15": "iso-iec-2382-15",
|
|
153
|
+
"iso-iec-25001": "iso-iec-25001",
|
|
154
|
+
"iso-iec-25012": "iso-iec-25012",
|
|
155
|
+
"iso-iec-9075-2": "iso-iec-9075-2",
|
|
156
|
+
"iso-iec-guide-2": "iso-iec-guide-2",
|
|
157
|
+
"iso-iec-guide-98-3": "iso-iec-guide-98-3",
|
|
158
|
+
"iso-iec-guide-99": "iso-iec-guide-99",
|
|
159
|
+
"iso-iec-ieee-24765": "iso-iec-ieee-24765",
|
|
160
|
+
"iso-iec-ieee-9945": "iso-iec-ieee-9945",
|
|
161
|
+
"iso-tr-19120": "iso-tr-19120",
|
|
162
|
+
"iso-ts-19104": "iso-ts-19104",
|
|
163
|
+
"iso-ts-19124-1": "iso-ts-19124-1",
|
|
164
|
+
"iso-ts-19129": "iso-ts-19129",
|
|
165
|
+
"iso-ts-19130": "iso-ts-19130",
|
|
166
|
+
"iso-ts-19130-2": "iso-ts-19130-2",
|
|
167
|
+
"iso-ts-19138": "iso-ts-19138",
|
|
168
|
+
"iso-ts-19158": "iso-ts-19158",
|
|
169
|
+
"iso-ts-19159-1": "iso-ts-19159-1",
|
|
170
|
+
"iso-ts-19159-2": "iso-ts-19159-2",
|
|
171
|
+
"iso-ts-19159-3": "iso-ts-19159-3",
|
|
172
|
+
"iso-ts-19159-4": "iso-ts-19159-4",
|
|
173
|
+
"iso-ts-19163-1": "iso-ts-19163-1",
|
|
174
|
+
"iso-ts-27790": "iso-ts-27790",
|
|
175
|
+
"matematicas": "matematicas",
|
|
176
|
+
"mathematics": "mathematics",
|
|
177
|
+
"mathematics, computer science": "mathematics-computer-science",
|
|
178
|
+
"mathematics, programming": "mathematics-programming",
|
|
179
|
+
"metric": "metric",
|
|
180
|
+
"metric, topology, geometry": "metric-topology-geometry",
|
|
181
|
+
"moving features": "moving-features",
|
|
182
|
+
"one parameter set of geometries": "one-parameter-set-of-geometries",
|
|
183
|
+
"ontology": "ontology",
|
|
184
|
+
"paikkatieto": "paikkatieto",
|
|
185
|
+
"paikkatietojakauma": "paikkatietojakauma",
|
|
186
|
+
"postal": "postal",
|
|
187
|
+
"postal address": "postal-address",
|
|
188
|
+
"programming": "programming",
|
|
189
|
+
"reference": "reference",
|
|
190
|
+
"referencia": "referencia",
|
|
191
|
+
"register": "register",
|
|
192
|
+
"registro": "registro",
|
|
193
|
+
"temporal": "temporal",
|
|
194
|
+
"topology": "topology",
|
|
195
|
+
"topology, geometry": "topology-geometry",
|
|
196
|
+
"topology, graph theory": "topology-graph-theory",
|
|
197
|
+
"topology, metric spaces": "topology-metric-spaces",
|
|
198
|
+
"user": "user",
|
|
199
|
+
"vector geometry": "vector-geometry",
|
|
200
|
+
"vocabulario general": "vocabulario-general"
|
|
201
|
+
}
|
|
@@ -548,7 +548,7 @@ describe('DatasetAdapter', () => {
|
|
|
548
548
|
expect(edges.length).toBe(2);
|
|
549
549
|
expect(edges.every(e => e.type === 'domain')).toBe(true);
|
|
550
550
|
expect(edges.find(e => e.lang === 'eng')?.target).toContain('/domain/geometry');
|
|
551
|
-
expect(edges.find(e => e.lang === 'fra')?.target).toContain('/domain/
|
|
551
|
+
expect(edges.find(e => e.lang === 'fra')?.target).toContain('/domain/géométrie');
|
|
552
552
|
expect(edges.find(e => e.lang === 'eng')?.label).toBe('geometry');
|
|
553
553
|
expect(edges.find(e => e.lang === 'fra')?.label).toBe('géométrie');
|
|
554
554
|
});
|
|
@@ -103,4 +103,19 @@ describe('conceptsToCsv (glossarist output API)', () => {
|
|
|
103
103
|
expect(rows[1]![0]).toBe('222-02-02');
|
|
104
104
|
expect(rows[1]![4]).toBe('');
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
// generate-data passes conceptUriPrefix = buildConceptUriPrefix(uriBase,
|
|
108
|
+
// register) so the uri column carries the SPA route shape
|
|
109
|
+
// (/dataset/<register>/concept/<id>) and opens in a browser — the
|
|
110
|
+
// RDF-canonical fallback would 404.
|
|
111
|
+
it('uri column carries the SPA route shape via conceptUriPrefix', () => {
|
|
112
|
+
const bare = new Concept({ id: '1-1-010', termid: '1-1-010', status: 'valid' });
|
|
113
|
+
const csv = conceptsToCsv([bare], {
|
|
114
|
+
uriBase: 'https://www.glossarist.org/iala-vocab',
|
|
115
|
+
registerId: 'iala-2023',
|
|
116
|
+
conceptUriPrefix: 'https://www.glossarist.org/iala-vocab/dataset/iala-2023/concept/',
|
|
117
|
+
});
|
|
118
|
+
const rows = parseCsv(csv);
|
|
119
|
+
expect(rows[1]![1]).toBe('https://www.glossarist.org/iala-vocab/dataset/iala-2023/concept/1-1-010');
|
|
120
|
+
});
|
|
106
121
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { slugify } from '../utils/slugify';
|
|
3
|
+
import publishedSlugs from '../__fixtures__/isotc211-domain-slugs.json';
|
|
3
4
|
|
|
4
5
|
describe('slugify', () => {
|
|
5
6
|
it('lowercases text', () => {
|
|
@@ -11,7 +12,7 @@ describe('slugify', () => {
|
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
it('strips non-word characters', () => {
|
|
14
|
-
expect(slugify('géométrie (2D)')).toBe('
|
|
15
|
+
expect(slugify('géométrie (2D)')).toBe('géométrie-2d');
|
|
15
16
|
});
|
|
16
17
|
|
|
17
18
|
it('collapses multiple spaces into single hyphen', () => {
|
|
@@ -25,4 +26,76 @@ describe('slugify', () => {
|
|
|
25
26
|
it('preserves hyphens in input', () => {
|
|
26
27
|
expect(slugify('iso-19107')).toBe('iso-19107');
|
|
27
28
|
});
|
|
29
|
+
|
|
30
|
+
describe('unicode labels', () => {
|
|
31
|
+
it('keeps accented letters', () => {
|
|
32
|
+
expect(slugify('dirección postal')).toBe('dirección-postal');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('keeps non-Latin scripts', () => {
|
|
36
|
+
expect(slugify('геопозиционирование')).toBe('геопозиционирование');
|
|
37
|
+
expect(slugify('地理定位')).toBe('地理定位');
|
|
38
|
+
expect(slugify('지리 위치')).toBe('지리-위치');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('keeps Arabic combining marks', () => {
|
|
42
|
+
expect(slugify('نظام الإحداثيات')).toBe('نظام-الإحداثيات');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('is NFC-stable: composed and decomposed input give one slug', () => {
|
|
46
|
+
expect(slugify('é')).toBe(slugify('é'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('gives distinct labels distinct slugs', () => {
|
|
50
|
+
const labels = ['UML', 'dirección postal', 'direccion postal', 'геопозиционирование',
|
|
51
|
+
'地理定位', '지리 위치', 'تحديد المواقع'];
|
|
52
|
+
expect(new Set(labels.map(slugify)).size).toBe(labels.length);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('is idempotent', () => {
|
|
56
|
+
for (const l of ['dirección postal', 'геопозиционирование', '()', 'a b']) {
|
|
57
|
+
expect(slugify(slugify(l))).toBe(slugify(l));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('labels without letters or digits', () => {
|
|
63
|
+
it('never returns an empty or separator-only slug', () => {
|
|
64
|
+
for (const l of ['()', '---', ' ', '!!', '/', '- -']) {
|
|
65
|
+
expect(slugify(l)).toMatch(/^x-[0-9a-f]{8}$/);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('distinguishes different such labels', () => {
|
|
70
|
+
expect(slugify('()')).not.toBe(slugify('[]'));
|
|
71
|
+
expect(slugify('()')).toBe(slugify('()'));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('still maps empty input to empty (callers guard on truthy)', () => {
|
|
75
|
+
expect(slugify('')).toBe('');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('existing ASCII URLs do not move', () => {
|
|
80
|
+
// Verbatim copy of the pre-unicode implementation.
|
|
81
|
+
const legacy = (t: string) => t.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
|
|
82
|
+
|
|
83
|
+
it('matches every domain slug published in the isotc211 dataset', () => {
|
|
84
|
+
const entries = Object.entries(publishedSlugs as Record<string, string>);
|
|
85
|
+
expect(entries.length).toBeGreaterThan(150);
|
|
86
|
+
for (const [label, slug] of entries) expect(slugify(label)).toBe(slug);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('is byte-identical to the legacy output for ASCII input with a letter or digit', () => {
|
|
90
|
+
let seed = 12345;
|
|
91
|
+
const rnd = () => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
|
|
92
|
+
for (let i = 0; i < 20000; i++) {
|
|
93
|
+
const len = 1 + Math.floor(rnd() * 12);
|
|
94
|
+
let t = '';
|
|
95
|
+
for (let j = 0; j < len; j++) t += String.fromCharCode(9 + Math.floor(rnd() * 118));
|
|
96
|
+
if (!/[A-Za-z0-9]/.test(t)) continue;
|
|
97
|
+
expect(slugify(t)).toBe(legacy(t));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
28
101
|
});
|
package/src/utils/slugify.ts
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
// Slug for a domain label. Keeps letters and digits of every script so that
|
|
2
|
+
// non-Latin and accented labels stay distinct and readable. Pure-ASCII input
|
|
3
|
+
// that contains a letter or digit yields exactly what the earlier ASCII-only
|
|
4
|
+
// implementation did, so already-published URLs do not move.
|
|
5
|
+
|
|
6
|
+
const KEEP = /[^\p{L}\p{N}\p{M}_\s-]/gu;
|
|
7
|
+
const HAS_WORD = /[\p{L}\p{N}]/u;
|
|
8
|
+
|
|
9
|
+
// FNV-1a, 32 bit, over UTF-16 code units. Sync and dependency-free so it runs
|
|
10
|
+
// in the browser and in the Node build scripts alike.
|
|
11
|
+
function shortHash(text: string): string {
|
|
12
|
+
let h = 0x811c9dc5;
|
|
13
|
+
for (let i = 0; i < text.length; i++) {
|
|
14
|
+
h ^= text.charCodeAt(i);
|
|
15
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
16
|
+
}
|
|
17
|
+
return h.toString(16).padStart(8, '0');
|
|
18
|
+
}
|
|
19
|
+
|
|
1
20
|
export function slugify(text: string): string {
|
|
2
|
-
|
|
21
|
+
if (text === '') return '';
|
|
22
|
+
const folded = text.normalize('NFKC').toLowerCase();
|
|
23
|
+
const slug = folded.replace(KEEP, '').replace(/[\s/]+/g, '-');
|
|
24
|
+
// No letter or digit left (e.g. "()", "---"): a hash keeps such labels
|
|
25
|
+
// from collapsing into one empty node.
|
|
26
|
+
return HAS_WORD.test(slug) ? slug : `x-${shortHash(folded.trim())}`;
|
|
3
27
|
}
|