@unrdf/kgn 5.0.1 → 26.4.2
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/dist/index.mjs +9207 -0
- package/package.json +33 -28
- package/src/base/filter-templates.js +7 -1
- package/src/base/index.js +15 -10
- package/src/base/injection-targets.js +6 -0
- package/src/base/macro-templates.js +6 -0
- package/src/base/shacl-templates.js +6 -0
- package/src/base/template-base.js +7 -1
- package/src/core/attestor.js +50 -1
- package/src/core/filters.js +134 -1
- package/src/core/index.js +8 -1
- package/src/core/kgen-engine.js +49 -1
- package/src/core/parser.js +52 -1
- package/src/core/post-processor.js +7 -1
- package/src/core/renderer.js +67 -1
- package/src/doc-generator/mdx-generator.mjs +1 -1
- package/src/doc-generator/nav-generator.mjs +1 -1
- package/src/doc-generator/rdf-builder.mjs +2 -2
- package/src/engine/index.js +9 -0
- package/src/engine/pipeline.js +7 -1
- package/src/engine/renderer.js +18 -3
- package/src/engine/template-engine.js +12 -3
- package/src/filters/array.js +14 -6
- package/src/filters/index.js +165 -17
- package/src/filters/rdf.js +3 -3
- package/src/{index.js → index.mjs} +46 -0
- package/src/index.test.mjs +40 -0
- package/src/inheritance/index.js +19 -1
- package/src/injection/atomic-writer.js +22 -1
- package/src/injection/idempotency-manager.js +33 -0
- package/src/injection/injection-engine.js +46 -1
- package/src/injection/integration.js +3 -3
- package/src/injection/modes/index.js +30 -0
- package/src/injection/rollback-manager.js +26 -2
- package/src/injection/target-resolver.js +48 -3
- package/src/injection/tests/injection-engine.test.js +3 -3
- package/src/injection/tests/integration.test.js +2 -1
- package/src/injection/tests/run-tests.js +3 -0
- package/src/injection/validation-engine.js +71 -5
- package/src/linter/determinism-linter.js +20 -5
- package/src/linter/determinism.js +8 -2
- package/src/linter/index.js +3 -1
- package/src/linter/test-doubles.js +151 -4
- package/src/parser/frontmatter.js +6 -0
- package/src/parser/variables.js +7 -1
- package/src/rdf/filters.js +393 -0
- package/src/rdf/index.js +444 -0
- package/src/renderer/deterministic.js +6 -0
- package/src/renderer/index.js +3 -1
- package/src/templates/rdf/DELIVERY-SUMMARY.md +266 -0
- package/src/templates/rdf/README.md +595 -0
- package/src/templates/rdf/dataset.njk +83 -0
- package/src/templates/rdf/index.js +106 -0
- package/src/templates/rdf/jsonld-context.njk +63 -0
- package/src/templates/rdf/ontology.njk +107 -0
- package/src/templates/rdf/schema.njk +79 -0
- package/src/templates/rdf/shapes.njk +89 -0
- package/src/templates/rdf/sparql-queries.njk +70 -0
- package/src/templates/rdf/vocabulary.njk +79 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RDF Template Filters - @unrdf/core Integration
|
|
3
|
+
* @module @unrdf/kgn/rdf/filters
|
|
4
|
+
* @description Custom Nunjucks filters for RDF operations using @unrdf/core
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import {
|
|
9
|
+
namedNode,
|
|
10
|
+
literal as createRdfLiteral,
|
|
11
|
+
blankNode as createRdfBlankNode,
|
|
12
|
+
quad,
|
|
13
|
+
COMMON_PREFIXES,
|
|
14
|
+
} from '@unrdf/core';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Schema for toTurtle filter options
|
|
18
|
+
*/
|
|
19
|
+
const ToTurtleOptionsSchema = z.object({
|
|
20
|
+
prefixes: z.record(z.string()).optional(),
|
|
21
|
+
indent: z.string().optional(),
|
|
22
|
+
includeComments: z.boolean().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Schema for toSparql filter options
|
|
27
|
+
*/
|
|
28
|
+
const ToSparqlOptionsSchema = z.object({
|
|
29
|
+
prefixes: z.record(z.string()).optional(),
|
|
30
|
+
type: z.enum(['select', 'construct', 'ask', 'describe']).optional(),
|
|
31
|
+
limit: z.number().int().positive().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Schema for RDF prefix configuration
|
|
36
|
+
*/
|
|
37
|
+
const RdfPrefixSchema = z.object({
|
|
38
|
+
uri: z.string().url(),
|
|
39
|
+
prefix: z.string().min(1),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Schema for blank node ID
|
|
44
|
+
*/
|
|
45
|
+
const BlankNodeIdSchema = z.string().regex(/^[a-zA-Z0-9_]+$/, 'Blank node ID must be alphanumeric with underscores');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Schema for literal options
|
|
49
|
+
*/
|
|
50
|
+
const LiteralOptionsSchema = z.object({
|
|
51
|
+
value: z.any(),
|
|
52
|
+
lang: z.string().regex(/^[a-z]{2,3}(-[A-Z]{2})?$/).optional(),
|
|
53
|
+
datatype: z.string().optional(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert RDF data to Turtle format
|
|
58
|
+
* @param {Object|Array} data - RDF quads or triples
|
|
59
|
+
* @param {Object} options - Formatting options
|
|
60
|
+
* @returns {string} Turtle-formatted RDF
|
|
61
|
+
* @example
|
|
62
|
+
* {{ rdfData | toTurtle }}
|
|
63
|
+
* {{ rdfData | toTurtle({ prefixes: { ex: 'http://example.org/' } }) }}
|
|
64
|
+
*/
|
|
65
|
+
export function toTurtle(data, options = {}) {
|
|
66
|
+
const validated = ToTurtleOptionsSchema.parse(options);
|
|
67
|
+
const {
|
|
68
|
+
prefixes = {},
|
|
69
|
+
indent = ' ',
|
|
70
|
+
includeComments = false
|
|
71
|
+
} = validated;
|
|
72
|
+
|
|
73
|
+
if (!data) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Merge with common prefixes
|
|
78
|
+
const allPrefixes = { ...COMMON_PREFIXES, ...prefixes };
|
|
79
|
+
|
|
80
|
+
const lines = [];
|
|
81
|
+
|
|
82
|
+
// Add prefix declarations
|
|
83
|
+
if (includeComments) {
|
|
84
|
+
lines.push('# RDF Turtle Format');
|
|
85
|
+
lines.push('');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const [prefix, uri] of Object.entries(allPrefixes)) {
|
|
89
|
+
lines.push(`@prefix ${prefix}: <${uri}> .`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (Object.keys(allPrefixes).length > 0) {
|
|
93
|
+
lines.push('');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Process quads/triples
|
|
97
|
+
const quads = Array.isArray(data) ? data : [data];
|
|
98
|
+
|
|
99
|
+
for (const item of quads) {
|
|
100
|
+
if (!item || typeof item !== 'object') {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const subject = formatTerm(item.subject || item.s, allPrefixes);
|
|
105
|
+
const predicate = formatTerm(item.predicate || item.p, allPrefixes);
|
|
106
|
+
const object = formatTerm(item.object || item.o, allPrefixes);
|
|
107
|
+
|
|
108
|
+
lines.push(`${subject} ${predicate} ${object} .`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generate SPARQL query from template pattern
|
|
116
|
+
* @param {Object|string} pattern - Query pattern or template
|
|
117
|
+
* @param {Object} options - Query options
|
|
118
|
+
* @returns {string} SPARQL query string
|
|
119
|
+
* @example
|
|
120
|
+
* {{ { s: '?s', p: 'rdf:type', o: '?type' } | toSparql }}
|
|
121
|
+
* {{ pattern | toSparql({ type: 'select', limit: 10 }) }}
|
|
122
|
+
*/
|
|
123
|
+
export function toSparql(pattern, options = {}) {
|
|
124
|
+
const validated = ToSparqlOptionsSchema.parse(options);
|
|
125
|
+
const {
|
|
126
|
+
prefixes = {},
|
|
127
|
+
type = 'select',
|
|
128
|
+
limit
|
|
129
|
+
} = validated;
|
|
130
|
+
|
|
131
|
+
// Merge with common prefixes
|
|
132
|
+
const allPrefixes = { ...COMMON_PREFIXES, ...prefixes };
|
|
133
|
+
|
|
134
|
+
const lines = [];
|
|
135
|
+
|
|
136
|
+
// Add prefix declarations
|
|
137
|
+
for (const [prefix, uri] of Object.entries(allPrefixes)) {
|
|
138
|
+
lines.push(`PREFIX ${prefix}: <${uri}>`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (Object.keys(allPrefixes).length > 0) {
|
|
142
|
+
lines.push('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle string pattern (raw SPARQL)
|
|
146
|
+
if (typeof pattern === 'string') {
|
|
147
|
+
lines.push(pattern);
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Build query from pattern object
|
|
152
|
+
if (type === 'select') {
|
|
153
|
+
const vars = extractVariables(pattern);
|
|
154
|
+
lines.push(`SELECT ${vars.join(' ')}`);
|
|
155
|
+
} else if (type === 'construct') {
|
|
156
|
+
lines.push('CONSTRUCT {');
|
|
157
|
+
lines.push(` ${formatTriplePattern(pattern, allPrefixes)}`);
|
|
158
|
+
lines.push('}');
|
|
159
|
+
} else if (type === 'ask') {
|
|
160
|
+
lines.push('ASK');
|
|
161
|
+
} else if (type === 'describe') {
|
|
162
|
+
const subject = pattern.subject || pattern.s || '?s';
|
|
163
|
+
lines.push(`DESCRIBE ${subject}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
lines.push('WHERE {');
|
|
167
|
+
|
|
168
|
+
if (Array.isArray(pattern)) {
|
|
169
|
+
for (const triple of pattern) {
|
|
170
|
+
lines.push(` ${formatTriplePattern(triple, allPrefixes)} .`);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
lines.push(` ${formatTriplePattern(pattern, allPrefixes)} .`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
lines.push('}');
|
|
177
|
+
|
|
178
|
+
if (limit) {
|
|
179
|
+
lines.push(`LIMIT ${limit}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Manage RDF prefix mappings
|
|
187
|
+
* @param {string} uri - Full URI to convert
|
|
188
|
+
* @param {string} prefix - Prefix to use
|
|
189
|
+
* @returns {string} Prefixed URI (CURIE)
|
|
190
|
+
* @example
|
|
191
|
+
* {{ 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' | rdfPrefix('rdf') }}
|
|
192
|
+
* // Returns: "rdf:type"
|
|
193
|
+
*/
|
|
194
|
+
export function rdfPrefix(uri, prefix) {
|
|
195
|
+
const validated = RdfPrefixSchema.parse({ uri, prefix });
|
|
196
|
+
|
|
197
|
+
const allPrefixes = { ...COMMON_PREFIXES };
|
|
198
|
+
|
|
199
|
+
// Check if prefix is known
|
|
200
|
+
if (allPrefixes[validated.prefix]) {
|
|
201
|
+
const namespace = allPrefixes[validated.prefix];
|
|
202
|
+
if (validated.uri.startsWith(namespace)) {
|
|
203
|
+
const localName = validated.uri.substring(namespace.length);
|
|
204
|
+
return `${validated.prefix}:${localName}`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// If URI doesn't match known prefix, return as-is
|
|
209
|
+
return validated.uri;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate blank node identifier
|
|
214
|
+
* @param {string} id - Optional identifier
|
|
215
|
+
* @returns {string} Blank node with _: prefix
|
|
216
|
+
* @example
|
|
217
|
+
* {{ 'person1' | blankNode }}
|
|
218
|
+
* // Returns: "_:person1"
|
|
219
|
+
*/
|
|
220
|
+
export function blankNode(id) {
|
|
221
|
+
if (!id) {
|
|
222
|
+
// Generate a deterministic blank node ID based on timestamp
|
|
223
|
+
const timestamp = Date.now();
|
|
224
|
+
return `_:blank${timestamp % 100000}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const validated = BlankNodeIdSchema.parse(id);
|
|
228
|
+
return `_:${validated}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create RDF literal with language tag or datatype
|
|
233
|
+
* @param {any} value - Literal value
|
|
234
|
+
* @param {string} lang - Language tag (e.g., 'en', 'fr')
|
|
235
|
+
* @param {string} datatype - Datatype URI (e.g., 'xsd:integer')
|
|
236
|
+
* @returns {string} RDF literal string
|
|
237
|
+
* @example
|
|
238
|
+
* {{ "Hello" | literal('en') }}
|
|
239
|
+
* // Returns: '"Hello"@en'
|
|
240
|
+
* {{ 42 | literal(null, 'xsd:integer') }}
|
|
241
|
+
* // Returns: '"42"^^xsd:integer'
|
|
242
|
+
*/
|
|
243
|
+
export function literal(value, lang = null, datatype = null) {
|
|
244
|
+
const validated = LiteralOptionsSchema.parse({
|
|
245
|
+
value,
|
|
246
|
+
lang: lang || undefined,
|
|
247
|
+
datatype: datatype || undefined,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (validated.value === null || validated.value === undefined) {
|
|
251
|
+
return '""';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Escape quotes in value
|
|
255
|
+
const stringValue = String(validated.value).replace(/"/g, '\\"');
|
|
256
|
+
let literalStr = `"${stringValue}"`;
|
|
257
|
+
|
|
258
|
+
if (validated.lang) {
|
|
259
|
+
literalStr += `@${validated.lang}`;
|
|
260
|
+
} else if (validated.datatype) {
|
|
261
|
+
literalStr += `^^${validated.datatype}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return literalStr;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Format RDF term for Turtle output
|
|
269
|
+
* @private
|
|
270
|
+
* @param {Object|string} term - RDF term
|
|
271
|
+
* @param {Object} prefixes - Prefix mappings
|
|
272
|
+
* @returns {string} Formatted term
|
|
273
|
+
*/
|
|
274
|
+
function formatTerm(term, prefixes = {}) {
|
|
275
|
+
if (!term) {
|
|
276
|
+
return '""';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (typeof term === 'string') {
|
|
280
|
+
// Variable
|
|
281
|
+
if (term.startsWith('?') || term.startsWith('$')) {
|
|
282
|
+
return term;
|
|
283
|
+
}
|
|
284
|
+
// Blank node
|
|
285
|
+
if (term.startsWith('_:')) {
|
|
286
|
+
return term;
|
|
287
|
+
}
|
|
288
|
+
// CURIE
|
|
289
|
+
if (term.includes(':') && !term.startsWith('http')) {
|
|
290
|
+
return term;
|
|
291
|
+
}
|
|
292
|
+
// URI
|
|
293
|
+
return `<${term}>`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (typeof term === 'object') {
|
|
297
|
+
// Named Node
|
|
298
|
+
if (term.termType === 'NamedNode' || term.type === 'NamedNode') {
|
|
299
|
+
const uri = term.value || term.uri;
|
|
300
|
+
return contractUri(uri, prefixes);
|
|
301
|
+
}
|
|
302
|
+
// Literal
|
|
303
|
+
if (term.termType === 'Literal' || term.type === 'Literal') {
|
|
304
|
+
let lit = `"${(term.value || '').replace(/"/g, '\\"')}"`;
|
|
305
|
+
if (term.language) {
|
|
306
|
+
lit += `@${term.language}`;
|
|
307
|
+
} else if (term.datatype && term.datatype.value !== 'http://www.w3.org/2001/XMLSchema#string') {
|
|
308
|
+
lit += `^^${contractUri(term.datatype.value, prefixes)}`;
|
|
309
|
+
}
|
|
310
|
+
return lit;
|
|
311
|
+
}
|
|
312
|
+
// Blank Node
|
|
313
|
+
if (term.termType === 'BlankNode' || term.type === 'BlankNode') {
|
|
314
|
+
return `_:${term.value || 'blank'}`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return String(term);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Contract URI to CURIE if possible
|
|
323
|
+
* @private
|
|
324
|
+
* @param {string} uri - Full URI
|
|
325
|
+
* @param {Object} prefixes - Prefix mappings
|
|
326
|
+
* @returns {string} CURIE or original URI
|
|
327
|
+
*/
|
|
328
|
+
function contractUri(uri, prefixes = {}) {
|
|
329
|
+
for (const [prefix, namespace] of Object.entries(prefixes)) {
|
|
330
|
+
if (uri.startsWith(namespace)) {
|
|
331
|
+
const localName = uri.substring(namespace.length);
|
|
332
|
+
return `${prefix}:${localName}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return `<${uri}>`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Format triple pattern for SPARQL
|
|
340
|
+
* @private
|
|
341
|
+
* @param {Object} pattern - Triple pattern
|
|
342
|
+
* @param {Object} prefixes - Prefix mappings
|
|
343
|
+
* @returns {string} Formatted triple pattern
|
|
344
|
+
*/
|
|
345
|
+
function formatTriplePattern(pattern, prefixes = {}) {
|
|
346
|
+
const subject = formatTerm(pattern.subject || pattern.s, prefixes);
|
|
347
|
+
const predicate = formatTerm(pattern.predicate || pattern.p, prefixes);
|
|
348
|
+
const object = formatTerm(pattern.object || pattern.o, prefixes);
|
|
349
|
+
|
|
350
|
+
return `${subject} ${predicate} ${object}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Extract SPARQL variables from pattern
|
|
355
|
+
* @private
|
|
356
|
+
* @param {Object|Array} pattern - Query pattern
|
|
357
|
+
* @returns {Array<string>} Variable names
|
|
358
|
+
*/
|
|
359
|
+
function extractVariables(pattern) {
|
|
360
|
+
const vars = new Set();
|
|
361
|
+
const patterns = Array.isArray(pattern) ? pattern : [pattern];
|
|
362
|
+
|
|
363
|
+
for (const p of patterns) {
|
|
364
|
+
if (!p || typeof p !== 'object') continue;
|
|
365
|
+
|
|
366
|
+
const terms = [
|
|
367
|
+
p.subject || p.s,
|
|
368
|
+
p.predicate || p.p,
|
|
369
|
+
p.object || p.o,
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
for (const term of terms) {
|
|
373
|
+
if (typeof term === 'string' && (term.startsWith('?') || term.startsWith('$'))) {
|
|
374
|
+
vars.add(term);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return vars.size > 0 ? Array.from(vars) : ['*'];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Export all RDF filters
|
|
384
|
+
*/
|
|
385
|
+
export const rdfTemplateFilters = {
|
|
386
|
+
toTurtle,
|
|
387
|
+
toSparql,
|
|
388
|
+
rdfPrefix,
|
|
389
|
+
blankNode,
|
|
390
|
+
literal,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export default rdfTemplateFilters;
|