glossarist 0.4.9 → 0.4.11

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.9",
3
+ "version": "0.4.11",
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",
@@ -10,7 +10,7 @@
10
10
  // to its relatives (the ontology declares these as owl:TransitiveProperty
11
11
  // for cascading section membership).
12
12
 
13
- import { namedNode, literal, quad } from './terms.js';
13
+ import { namedNode, blankNode, literal, quad } from './terms.js';
14
14
 
15
15
  // Prefix namespaces used by this emitter. Hard-coded (not via PREFIXES)
16
16
  // because dcat and prov are absent from the JSON-LD context that
@@ -138,8 +138,10 @@ export function* datasetToQuads(input) {
138
138
 
139
139
  function* distributionToQuads(ds, dist) {
140
140
  // Distributions are blank nodes — they don't have stable identity
141
- // outside their parent dataset.
142
- const distSubject = namedNode(`_:b${deterministicId('dist', dist.id, dist.downloadUrl)}`);
141
+ // outside their parent dataset. Using blankNode() (not namedNode with
142
+ // a "_:..." string) so the n3 writer serializes them as _:bXXXX
143
+ // rather than <_:bXXXX> which would be an invalid IRI.
144
+ const distSubject = blankNode(`b${deterministicId('dist', dist.id, dist.downloadUrl)}`);
143
145
  yield quad(ds, namedNode(DCAT.distribution), distSubject);
144
146
  yield quad(distSubject, namedNode(RDF_TYPE), namedNode(DCAT.Distribution));
145
147
  yield quad(distSubject, namedNode(DCTERMS.title), literal(dist.title));
@@ -256,6 +256,7 @@ export interface WriteTurtleOptions {
256
256
  }
257
257
 
258
258
  export declare function writeTurtle(quads: Iterable<Quad>, options?: WriteTurtleOptions): Promise<string>;
259
+ export declare function writeTurtleSync(quads: Iterable<Quad>, options?: WriteTurtleOptions): string;
259
260
  export declare function writeNTriples(quads: Iterable<Quad>): Promise<string>;
260
261
 
261
262
  export interface WriteJsonldOptions {
package/src/rdf/index.js CHANGED
@@ -41,4 +41,5 @@ export {
41
41
  writeJsonld,
42
42
  sortQuads,
43
43
  } from './document-writer.js';
44
+ export { writeTurtleSync } from './write-turtle-sync.js';
44
45
  export { validateShacl, loadShapes, clearShapesCache, quadsToDataset } from './shacl.js';
@@ -0,0 +1,166 @@
1
+ // Sync Turtle writer — produces Turtle from RDF/JS Quads without
2
+ // n3's callback API. Designed for UI use cases (Vue computed, React
3
+ // render) that need synchronous output.
4
+ //
5
+ // Format matches n3's Turtle output closely but may differ in:
6
+ // - Indent width (2 spaces vs n3's 4)
7
+ // - Blank node label format (_:bXXXX)
8
+ // - Subject ordering (sorted by value for determinism)
9
+ //
10
+ // For byte-equivalent n3 output, use the async writeTurtle() instead.
11
+
12
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
13
+
14
+ /**
15
+ * Serializes RDF/JS Quads to a Turtle string synchronously.
16
+ *
17
+ * @param {Iterable<Quad>} quads
18
+ * @param {{ prefixes?: Record<string, string> }} [options]
19
+ * @returns {string}
20
+ */
21
+ export function writeTurtleSync(quads, options = {}) {
22
+ const prefixes = options.prefixes ?? {};
23
+ const sorted = [...quads].sort(compareQuadSync);
24
+ const grouped = groupBySubject(sorted);
25
+
26
+ const lines = [];
27
+
28
+ // Prefix declarations — sorted by prefix name for deterministic output.
29
+ for (const [prefix, iri] of Object.entries(prefixes).sort(([a], [b]) => a.localeCompare(b))) {
30
+ lines.push(`@prefix ${prefix}: <${iri}> .`);
31
+ }
32
+
33
+ for (const { subject, triples } of grouped) {
34
+ lines.push('');
35
+ lines.push(...writeResource(subject, triples, prefixes));
36
+ }
37
+
38
+ return lines.join('\n') + '\n';
39
+ }
40
+
41
+ function writeResource(subject, triples, prefixes) {
42
+ const subjectStr = formatTermSync(subject, prefixes);
43
+ const types = triples.filter(t => t.predicate.value === RDF_TYPE);
44
+ const others = triples.filter(t => t.predicate.value !== RDF_TYPE);
45
+
46
+ if (types.length === 0 && others.length === 0) {
47
+ return [`${subjectStr} .`];
48
+ }
49
+
50
+ const lines = [];
51
+ const allPredObjs = [];
52
+
53
+ if (types.length > 0) {
54
+ const typeStrs = types.map(t => formatTermSync(t.object, prefixes));
55
+ allPredObjs.push({ predStr: 'a', objStrs: typeStrs });
56
+ }
57
+
58
+ // Group by predicate for multi-value compaction (predicate only appears once
59
+ // with comma-separated objects).
60
+ const byPredicate = new Map();
61
+ for (const t of others) {
62
+ const predStr = formatTermSync(t.predicate, prefixes);
63
+ if (!byPredicate.has(predStr)) byPredicate.set(predStr, []);
64
+ byPredicate.get(predStr).push(formatTermSync(t.object, prefixes));
65
+ }
66
+ for (const [predStr, objStrs] of byPredicate) {
67
+ allPredObjs.push({ predStr, objStrs });
68
+ }
69
+
70
+ lines.push(`${subjectStr} ${allPredObjs[0].predStr} ${allPredObjs[0].objStrs.join(', ')} ;`);
71
+ for (let i = 1; i < allPredObjs.length; i++) {
72
+ lines.push(` ${allPredObjs[i].predStr} ${allPredObjs[i].objStrs.join(', ')} ;`);
73
+ }
74
+ // Replace trailing ; with .
75
+ lines[lines.length - 1] = lines[lines.length - 1].replace(/ ;$/, ' .');
76
+ return lines;
77
+ }
78
+
79
+ function formatTermSync(term, prefixes) {
80
+ switch (term.termType) {
81
+ case 'NamedNode':
82
+ return compactIriSync(term.value, prefixes);
83
+ case 'BlankNode':
84
+ return `_:${term.value}`;
85
+ case 'Literal':
86
+ return formatLiteralSync(term);
87
+ case 'DefaultGraph':
88
+ return '';
89
+ default:
90
+ return String(term.value ?? term);
91
+ }
92
+ }
93
+
94
+ function formatLiteralSync(lit) {
95
+ const escaped = escapeLiteralSync(lit.value);
96
+ if (lit.language) return `"${escaped}"@${lit.language}`;
97
+ const dt = lit.datatype?.value;
98
+ if (dt && dt !== 'http://www.w3.org/2001/XMLSchema#string') {
99
+ return `"${escaped}"^^<${dt}>`;
100
+ }
101
+ return `"${escaped}"`;
102
+ }
103
+
104
+ function escapeLiteralSync(s) {
105
+ return String(s)
106
+ .replace(/\\/g, '\\\\')
107
+ .replace(/"/g, '\\"')
108
+ .replace(/\n/g, '\\n')
109
+ .replace(/\r/g, '\\r')
110
+ .replace(/\t/g, '\\t');
111
+ }
112
+
113
+ function compactIriSync(iri, prefixes) {
114
+ // If no prefixes, always use <>
115
+ if (!prefixes || Object.keys(prefixes).length === 0) {
116
+ return `<${iri}>`;
117
+ }
118
+ // Try longest prefix match
119
+ let bestPrefix = null;
120
+ let bestLen = 0;
121
+ for (const [prefix, base] of Object.entries(prefixes)) {
122
+ if (iri.startsWith(base) && base.length > bestLen) {
123
+ // Reject if local part contains characters that can't appear in a
124
+ // Turtle pname-local (e.g. leading digit is fine per Turtle 1.1,
125
+ // but // is not).
126
+ const local = iri.slice(base.length);
127
+ if (!local.startsWith('//')) {
128
+ bestPrefix = prefix;
129
+ bestLen = base.length;
130
+ }
131
+ }
132
+ }
133
+ if (!bestPrefix) return `<${iri}>`;
134
+ const local = iri.slice(bestLen);
135
+ const escaped = local.replace(/([/])/g, '\\$1');
136
+ return `${bestPrefix}:${escaped}`;
137
+ }
138
+
139
+ function compareQuadSync(a, b) {
140
+ let cmp = cmpTermSync(a.subject, b.subject);
141
+ if (cmp !== 0) return cmp;
142
+ cmp = cmpTermSync(a.predicate, b.predicate);
143
+ if (cmp !== 0) return cmp;
144
+ return cmpTermSync(a.object, b.object);
145
+ }
146
+
147
+ function cmpTermSync(a, b) {
148
+ if (a.termType !== b.termType) return a.termType.localeCompare(b.termType);
149
+ return String(a.value).localeCompare(String(b.value));
150
+ }
151
+
152
+ function groupBySubject(quads) {
153
+ const map = new Map();
154
+ for (const q of quads) {
155
+ const key = termKey(q.subject);
156
+ if (!map.has(key)) {
157
+ map.set(key, { subject: q.subject, triples: [] });
158
+ }
159
+ map.get(key).triples.push({ predicate: q.predicate, object: q.object });
160
+ }
161
+ return [...map.values()];
162
+ }
163
+
164
+ function termKey(term) {
165
+ return `${term.termType}:${term.value}`;
166
+ }