@glossarist/concept-browser 0.7.138 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.138",
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": {
@@ -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"];
@@ -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) {
@@ -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/gomtrie');
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
  });
@@ -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('gomtrie-2d');
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
  });
@@ -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
- return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
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
  }