glossarist 0.4.13 → 0.4.15

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",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "description": "JavaScript SDK for Glossarist GCR packages — read, write, validate, and manage terminology concepts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -42,6 +42,11 @@
42
42
  "import": "./src/rdf/index.js",
43
43
  "default": "./src/rdf/index.js"
44
44
  },
45
+ "./rdf/shacl": {
46
+ "types": "./src/rdf/shacl-entry.d.ts",
47
+ "import": "./src/rdf/shacl-entry.js",
48
+ "default": "./src/rdf/shacl-entry.js"
49
+ },
45
50
  "./rdf/prefixes": {
46
51
  "types": "./src/rdf/predicates.d.ts",
47
52
  "import": "./src/rdf/predicates.js",
@@ -6,11 +6,30 @@
6
6
  // strategy: MD5 (via node:crypto) when available, FNV-1a fallback for
7
7
  // browser bundles.
8
8
  //
9
- // Cross-process stability matches glossarist-ruby's deterministic-id
10
- // when node:crypto is available. Within a single process, FNV-1a is
11
- // also stable.
9
+ // Browser safety: node:crypto is loaded lazily via runtime check so
10
+ // bundlers don't tree-shake the import into the browser bundle.
12
11
 
13
- import { createHash } from 'node:crypto';
12
+ let nodeCreateHash = null;
13
+ let nodeCryptoChecked = false;
14
+
15
+ function getNodeCreateHash() {
16
+ if (nodeCryptoChecked) return nodeCreateHash;
17
+ nodeCryptoChecked = true;
18
+ try {
19
+ let requireFn;
20
+ try {
21
+ const mod = (Function('m', 'return require(m)'))('node:module');
22
+ requireFn = mod.createRequire(import.meta.url);
23
+ } catch {
24
+ requireFn = (m) => (Function('m', 'return require(m)'))(m);
25
+ }
26
+ const crypto = requireFn('node:crypto');
27
+ nodeCreateHash = crypto.createHash.bind(crypto);
28
+ } catch {
29
+ nodeCreateHash = null;
30
+ }
31
+ return nodeCreateHash;
32
+ }
14
33
 
15
34
  function fnv1a(seed) {
16
35
  let h = 0x811c9dc5;
@@ -24,12 +43,13 @@ function fnv1a(seed) {
24
43
  export function deterministicBnodeId(...parts) {
25
44
  const seed = parts.filter(p => p != null && p !== '').join('|');
26
45
  if (!seed) return `b${fnv1a('empty')}`;
27
- // MD5 matches glossarist-ruby's deterministic-id for cross-process
28
- // stability. Truncated to 12 hex chars — enough collision resistance
29
- // for bnode IDs within a single document.
30
- try {
31
- return createHash('md5').update(seed).digest('hex').slice(0, 12);
32
- } catch {
33
- return `b${fnv1a(seed)}`;
46
+ const createHash = getNodeCreateHash();
47
+ if (createHash) {
48
+ try {
49
+ return createHash('md5').update(seed).digest('hex').slice(0, 12);
50
+ } catch {
51
+ // fall through to FNV
52
+ }
34
53
  }
54
+ return `b${fnv1a(seed)}`;
35
55
  }
@@ -1,17 +1,71 @@
1
- import { createHash } from 'node:crypto';
1
+ // Stable ID helpers for RDF emission. Mirrors glossarist-ruby's
2
+ // Digest::MD5.hexdigest(content)[0..11] for cross-runtime stability.
3
+ //
4
+ // Browser safety: node:crypto is loaded lazily so bundlers don't
5
+ // tree-shake the import into browser bundles. The runtime check uses
6
+ // `createRequire` from `node:module` (also lazy) which is the
7
+ // documented way to use CommonJS `require` from ESM.
8
+
9
+ let nodeCreateHash = null;
10
+ let nodeCryptoChecked = false;
11
+
12
+ function getNodeCreateHash() {
13
+ if (nodeCryptoChecked) return nodeCreateHash;
14
+ nodeCryptoChecked = true;
15
+ try {
16
+ // Lazy require — works in both CJS and ESM (Node ≥ 12).
17
+ // Wrapped in Function() so bundlers don't statically resolve it.
18
+ let requireFn;
19
+ try {
20
+ // ESM: use createRequire from node:module
21
+ const mod = (Function('m', 'return require(m)'))('node:module');
22
+ requireFn = mod.createRequire(import.meta.url);
23
+ } catch {
24
+ // CJS or already-require-available environment
25
+ requireFn = (m) => (Function('m', 'return require(m)'))(m);
26
+ }
27
+ const crypto = requireFn('node:crypto');
28
+ nodeCreateHash = crypto.createHash.bind(crypto);
29
+ } catch {
30
+ nodeCreateHash = null;
31
+ }
32
+ return nodeCreateHash;
33
+ }
2
34
 
3
35
  // 12-char hex prefix, mirroring glossarist-ruby's
4
36
  // `Digest::MD5.hexdigest(content)[0..11]`. Same content → same ID across
5
- // processes, machines, and language runtimes (Ruby vs JS).
37
+ // processes, machines, and language runtimes (Ruby vs JS) when MD5 is
38
+ // available. Falls back to a 12-char FNV-1a hash in the browser.
6
39
  export function deterministicId(...parts) {
7
40
  const seed = parts.filter(p => p !== null && p !== undefined).join('|');
8
- return createHash('md5').update(seed).digest('hex').slice(0, 12);
41
+ const createHash = getNodeCreateHash();
42
+ if (createHash) {
43
+ return createHash('md5').update(seed).digest('hex').slice(0, 12);
44
+ }
45
+ return fnv1a12(seed);
9
46
  }
10
47
 
11
48
  // Stable blank-node label for an RDF fragment identified by its parent
12
- // subject + role + index. Useful for reified resources like DetailedDefinition,
13
- // Designation, ConceptSource where the bnode ID must be deterministic to
14
- // produce byte-equivalent Turtle across runs.
49
+ // subject + role + index.
15
50
  export function deterministicBnode(subject, role, index) {
16
51
  return `_:b${deterministicId(subject, role, index)}`;
17
52
  }
53
+
54
+ // 12-char FNV-1a hash. Two 32-bit passes concatenated and truncated.
55
+ // Not as collision-resistant as MD5 but adequate for bnode IDs in a
56
+ // single document. Used only when node:crypto is unavailable.
57
+ function fnv1a12(seed) {
58
+ let h1 = 0x811c9dc5;
59
+ for (let i = 0; i < seed.length; i++) {
60
+ h1 ^= seed.charCodeAt(i);
61
+ h1 = Math.imul(h1, 0x01000193);
62
+ }
63
+ const seed2 = `${seed}|fnv2`;
64
+ let h2 = 0x811c9dc5;
65
+ for (let i = 0; i < seed2.length; i++) {
66
+ h2 ^= seed2.charCodeAt(i);
67
+ h2 = Math.imul(h2, 0x01000193);
68
+ }
69
+ return ((h1 >>> 0).toString(16).padStart(8, '0')
70
+ + (h2 >>> 0).toString(16).padStart(8, '0')).slice(0, 12);
71
+ }
@@ -315,7 +315,5 @@ export interface ValidationReport {
315
315
  }>;
316
316
  }
317
317
 
318
- export declare function loadShapes(options?: { shapesPath?: string }): Promise<Dataset>;
319
- export declare function validateShacl(dataDataset: Dataset, options?: { shapes?: Dataset; shapesPath?: string }): Promise<ValidationReport>;
320
- export declare function clearShapesCache(): void;
321
- export declare function quadsToDataset(quads: Iterable<Quad>): Dataset;
318
+ // SHACL validator moved to ./shacl-entry subpath. Importing from
319
+ // 'glossarist/rdf/shacl' in Node-only; 'glossarist/rdf' is browser-safe.
package/src/rdf/index.js CHANGED
@@ -50,6 +50,8 @@ export {
50
50
  sortQuads,
51
51
  } from './document-writer.js';
52
52
  export { writeTurtleSync } from './write-turtle-sync.js';
53
- export { validateShacl, loadShapes, clearShapesCache, quadsToDataset } from './shacl.js';
53
+ // SHACL validator moved to glossarist/rdf/shacl subpath — it depends on
54
+ // node:fs/promises, node:path, node:url at module top-level, which would
55
+ // break browser bundles. Importing 'glossarist/rdf' alone is browser-safe.
54
56
  export { quadSectionsToClassInstances } from './sections-builder.js';
55
57
  export { provenanceToQuads } from './provenance-emitter.js';
@@ -100,11 +100,17 @@ function resourceToClassInstance(subject, quads, bnodeQuads, language) {
100
100
  }
101
101
 
102
102
  const subjectStr = subject.value;
103
- const classId = types.length > 0 ? types[0] : '';
104
- const classLabel = deriveClassLabel(classId);
103
+ // Compact classId and classLabel to CURIE form for UI consumers.
104
+ // The Vue components compare against CURIEs ('gloss:Concept') not
105
+ // absolute IRIs, so the builder is responsible for the conversion.
106
+ const rawClassId = types.length > 0 ? types[0] : '';
107
+ const classId = rawClassId ? compactIri(rawClassId) : '';
108
+ const classLabel = deriveClassLabel(rawClassId);
105
109
  const label = prefLabel?.value ?? rdfsLabel ?? deriveSubjectLabel(subjectStr);
106
110
 
107
- // Group predicates (preserve insertion order)
111
+ // Group predicates (preserve insertion order). Compact IRIs to CURIEs
112
+ // for display — UI consumers expect 'gloss:hasDefinition' rather than
113
+ // 'https://www.glossarist.org/ontologies/hasDefinition'.
108
114
  const props = [];
109
115
  const seenPredKeys = new Set();
110
116
 
@@ -112,11 +118,12 @@ function resourceToClassInstance(subject, quads, bnodeQuads, language) {
112
118
  const isBnodeObj = q.object.termType === 'BlankNode';
113
119
  const formatted = formatTerm(q.object, bnodeQuads);
114
120
  if (formatted === '') continue;
115
- const dedupKey = `${q.predicate.value}#${isBnodeObj ? 'n' : 'f'}#${formatted}`;
121
+ const predicate = compactIri(q.predicate.value);
122
+ const dedupKey = `${predicate}#${isBnodeObj ? 'n' : 'f'}#${formatted}`;
116
123
  if (seenPredKeys.has(dedupKey)) continue;
117
124
  seenPredKeys.add(dedupKey);
118
125
 
119
- const prop = { predicate: q.predicate.value, values: [formatted] };
126
+ const prop = { predicate, values: [formatted] };
120
127
  if (isBnodeObj) prop.nested = true;
121
128
  props.push(prop);
122
129
  }
@@ -132,7 +139,7 @@ function resourceToClassInstance(subject, quads, bnodeQuads, language) {
132
139
  function formatTerm(term, bnodeQuads) {
133
140
  switch (term.termType) {
134
141
  case 'NamedNode':
135
- return term.value;
142
+ return compactIri(term.value);
136
143
  case 'Literal':
137
144
  return term.value;
138
145
  case 'BlankNode': {
@@ -151,11 +158,11 @@ function formatTerm(term, bnodeQuads) {
151
158
 
152
159
  function deriveClassLabel(classId) {
153
160
  if (!classId) return '';
154
- // CURIE case: "gloss:Concept" → "Concept"
155
- if (!classId.includes('://') && classId.includes(':')) {
161
+ // ClassId is now CURIE form (compacted). Just take the local part.
162
+ if (classId.includes(':') && !classId.includes('://')) {
156
163
  return classId.slice(classId.indexOf(':') + 1);
157
164
  }
158
- // Absolute IRI: take last path segment, strip fragment/query
165
+ // Fallback for absolute IRIs that didn't compact
159
166
  const noFrag = classId.split('#').pop();
160
167
  const last = noFrag.split('/').pop();
161
168
  return last || classId;
@@ -0,0 +1,6 @@
1
+ // Type declarations for the Node-only SHACL validator subpath.
2
+ //
3
+ // Mirrors src/rdf/shacl-entry.js. Separated from ./rdf so that browser
4
+ // bundles don't pull in node:fs/promises, node:path, node:url.
5
+
6
+ export { validateShacl, loadShapes, clearShapesCache, quadsToDataset } from './shacl';
@@ -0,0 +1,9 @@
1
+ // Node-only SHACL validator entry. Separated from glossarist/rdf so that
2
+ // browser bundles can import RDF emitters/writers without pulling in
3
+ // node:fs/promises, node:path, node:url.
4
+ //
5
+ // Usage (Node only):
6
+ // import { validateShacl, loadShapes, clearShapesCache, quadsToDataset }
7
+ // from 'glossarist/rdf/shacl';
8
+
9
+ export { validateShacl, loadShapes, clearShapesCache, quadsToDataset } from './shacl.js';