glossarist 0.4.10 → 0.4.12
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 +1 -1
- package/src/rdf/agents-emitter.js +6 -4
- package/src/rdf/bibliography-emitter.js +6 -3
- package/src/rdf/build-activity-emitter.js +2 -1
- package/src/rdf/image-variant-emitter.js +5 -3
- package/src/rdf/index.d.ts +1 -0
- package/src/rdf/index.js +1 -0
- package/src/rdf/write-turtle-sync.js +166 -0
- package/src/transforms/concept-to-gloss.transform.js +4 -2
package/package.json
CHANGED
|
@@ -33,8 +33,8 @@ const RDFS = {
|
|
|
33
33
|
seeAlso: `${RDFS_NS}seeAlso`,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// No hardcoded default agent/org base URIs. Callers MUST pass agentBase
|
|
37
|
+
// explicitly so agent IRIs reflect the consumer's domain.
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* @typedef {Object} Contributor
|
|
@@ -64,7 +64,8 @@ export function slugify(input) {
|
|
|
64
64
|
* Builds AgentInput records from raw contributor declarations.
|
|
65
65
|
* Useful for callers that want to inspect the IRIs before emission.
|
|
66
66
|
*/
|
|
67
|
-
export function agentsFromContributors(contributors, agentBase
|
|
67
|
+
export function agentsFromContributors(contributors, agentBase) {
|
|
68
|
+
if (!agentBase) throw new Error('agentsFromContributors requires agentBase — the deployment canonical URI root for agents (e.g. https://www.example.org/agent). glossarist-js does NOT default to glossarist.org.');
|
|
68
69
|
return (contributors ?? []).map(c => {
|
|
69
70
|
const slug = slugify(c.name);
|
|
70
71
|
return {
|
|
@@ -92,7 +93,8 @@ export function agentsFromContributors(contributors, agentBase = DEFAULT_AGENT_B
|
|
|
92
93
|
* @returns {Generator<Quad, void, unknown>}
|
|
93
94
|
*/
|
|
94
95
|
export function* agentsToQuads(agents, options = {}) {
|
|
95
|
-
const orgBase = options.orgBase
|
|
96
|
+
const orgBase = options.orgBase;
|
|
97
|
+
if (!orgBase) throw new Error('agentsToQuads requires options.orgBase — the deployment canonical URI root for organizations.');
|
|
96
98
|
const emittedOrgs = new Set();
|
|
97
99
|
|
|
98
100
|
for (const a of agents ?? []) {
|
|
@@ -28,7 +28,8 @@ const FOAF = {
|
|
|
28
28
|
page: `${FOAF_NS}page`,
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// No hardcoded default base URI. Callers MUST pass baseUri explicitly
|
|
32
|
+
// so instance IRIs reflect the consumer's domain, not the library's.
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* @typedef {Object} BibliographyEntry
|
|
@@ -52,7 +53,8 @@ const DEFAULT_BASE_URI = 'https://glossarist.org';
|
|
|
52
53
|
* @param {string} entryId
|
|
53
54
|
* @param {string} [baseUri]
|
|
54
55
|
*/
|
|
55
|
-
export function bibliographyEntryIri(registerId, entryId, baseUri
|
|
56
|
+
export function bibliographyEntryIri(registerId, entryId, baseUri) {
|
|
57
|
+
if (!baseUri) throw new Error('bibliographyEntryIri requires baseUri — the deployment canonical URI root. glossarist-js does NOT default to glossarist.org because instance data identity must reflect the consumer domain.');
|
|
56
58
|
return `${baseUri}/${registerId}/bib/${entryId}`;
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -64,7 +66,8 @@ export function bibliographyEntryIri(registerId, entryId, baseUri = DEFAULT_BASE
|
|
|
64
66
|
* @returns {Generator<Quad, void, unknown>}
|
|
65
67
|
*/
|
|
66
68
|
export function* bibliographyToQuads(input) {
|
|
67
|
-
const baseUri = input.baseUri
|
|
69
|
+
const baseUri = input.baseUri;
|
|
70
|
+
if (!baseUri) throw new Error('bibliographyToQuads requires input.baseUri — the deployment canonical URI root.');
|
|
68
71
|
const parentIri = `${baseUri}/${input.registerId}/`;
|
|
69
72
|
|
|
70
73
|
for (const entry of input.entries ?? []) {
|
|
@@ -76,7 +76,8 @@ export function buildActivityIri(input) {
|
|
|
76
76
|
* @returns {Generator<Quad, void, unknown>}
|
|
77
77
|
*/
|
|
78
78
|
export function* buildActivityToQuads(input) {
|
|
79
|
-
const baseUri = input.baseUri
|
|
79
|
+
const baseUri = input.baseUri;
|
|
80
|
+
if (!baseUri) throw new Error('buildActivityToQuads requires input.baseUri — the deployment canonical URI root. glossarist-js does NOT default to glossarist.org because instance data identity must reflect the consumer domain.');
|
|
80
81
|
const activityIri = `${baseUri}/${buildActivityIri(input)}`;
|
|
81
82
|
const activity = namedNode(activityIri);
|
|
82
83
|
|
|
@@ -28,7 +28,7 @@ const DCAT = {
|
|
|
28
28
|
};
|
|
29
29
|
const XSD_INTEGER = `${XSD_NS}integer`;
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// No hardcoded default base URI. Callers MUST pass baseUri explicitly.
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* @typedef {'svg' | 'png' | 'webp' | 'jpg' | 'gif'} ImageFormat
|
|
@@ -55,7 +55,8 @@ const MIME_BY_FORMAT = Object.freeze({
|
|
|
55
55
|
* Returns the canonical IRI for an image variant. Includes the lang
|
|
56
56
|
* segment when the variant is localized, otherwise omits it.
|
|
57
57
|
*/
|
|
58
|
-
export function imageVariantIri(input, baseUri
|
|
58
|
+
export function imageVariantIri(input, baseUri) {
|
|
59
|
+
if (!baseUri) throw new Error('imageVariantIri requires baseUri — the deployment canonical URI root.');
|
|
59
60
|
const langSuffix = input.lang ? `${input.lang}.` : '';
|
|
60
61
|
return `${baseUri}/${input.registerId}/image/${input.figureId}/${langSuffix}${input.format}`;
|
|
61
62
|
}
|
|
@@ -63,7 +64,8 @@ export function imageVariantIri(input, baseUri = DEFAULT_BASE_URI) {
|
|
|
63
64
|
/**
|
|
64
65
|
* Emits foaf:Image quads for one image variant.
|
|
65
66
|
*/
|
|
66
|
-
export function* imageVariantToQuads(input, baseUri
|
|
67
|
+
export function* imageVariantToQuads(input, baseUri) {
|
|
68
|
+
if (!baseUri) throw new Error('imageVariantToQuads requires baseUri — the deployment canonical URI root.');
|
|
67
69
|
const img = namedNode(imageVariantIri(input, baseUri));
|
|
68
70
|
yield quad(img, namedNode(RDF_TYPE), namedNode(FOAF.Image));
|
|
69
71
|
yield quad(img, namedNode(DCTERMS.format), literal(MIME_BY_FORMAT[input.format] ?? 'application/octet-stream'));
|
package/src/rdf/index.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
writeJsonld,
|
|
17
17
|
} from '../rdf/document-writer.js';
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// No hardcoded default URI base. Callers MUST pass options.uriBase so
|
|
20
|
+
// concept IRIs reflect the consumer's domain.
|
|
20
21
|
|
|
21
22
|
// Writer registry. Each entry is `{ write: (quads, transform) => Promise<string> }`.
|
|
22
23
|
// Adding a new output format is a single entry here, not six new methods.
|
|
@@ -29,7 +30,8 @@ const WRITERS = Object.freeze({
|
|
|
29
30
|
export class ConceptToGlossTransform {
|
|
30
31
|
constructor(options = {}) {
|
|
31
32
|
this.registerId = options.registerId;
|
|
32
|
-
this.uriBase =
|
|
33
|
+
this.uriBase = options.uriBase?.replace(/\/+$/, '');
|
|
34
|
+
if (!this.uriBase) throw new Error('ConceptToGlossTransform requires options.uriBase — the deployment canonical URI root.');
|
|
33
35
|
this.jsonldContext = options.jsonldContext;
|
|
34
36
|
}
|
|
35
37
|
|