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 +6 -1
- package/src/rdf/bnode-id.js +31 -11
- package/src/rdf/deterministic-id.js +60 -6
- package/src/rdf/index.d.ts +2 -4
- package/src/rdf/index.js +3 -1
- package/src/rdf/sections-builder.js +16 -9
- package/src/rdf/shacl-entry.d.ts +6 -0
- package/src/rdf/shacl-entry.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glossarist",
|
|
3
|
-
"version": "0.4.
|
|
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",
|
package/src/rdf/bnode-id.js
CHANGED
|
@@ -6,11 +6,30 @@
|
|
|
6
6
|
// strategy: MD5 (via node:crypto) when available, FNV-1a fallback for
|
|
7
7
|
// browser bundles.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
package/src/rdf/index.d.ts
CHANGED
|
@@ -315,7 +315,5 @@ export interface ValidationReport {
|
|
|
315
315
|
}>;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
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
|
|
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
|
|
155
|
-
if (
|
|
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
|
-
//
|
|
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';
|