@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.
Files changed (59) hide show
  1. package/dist/index.mjs +9207 -0
  2. package/package.json +33 -28
  3. package/src/base/filter-templates.js +7 -1
  4. package/src/base/index.js +15 -10
  5. package/src/base/injection-targets.js +6 -0
  6. package/src/base/macro-templates.js +6 -0
  7. package/src/base/shacl-templates.js +6 -0
  8. package/src/base/template-base.js +7 -1
  9. package/src/core/attestor.js +50 -1
  10. package/src/core/filters.js +134 -1
  11. package/src/core/index.js +8 -1
  12. package/src/core/kgen-engine.js +49 -1
  13. package/src/core/parser.js +52 -1
  14. package/src/core/post-processor.js +7 -1
  15. package/src/core/renderer.js +67 -1
  16. package/src/doc-generator/mdx-generator.mjs +1 -1
  17. package/src/doc-generator/nav-generator.mjs +1 -1
  18. package/src/doc-generator/rdf-builder.mjs +2 -2
  19. package/src/engine/index.js +9 -0
  20. package/src/engine/pipeline.js +7 -1
  21. package/src/engine/renderer.js +18 -3
  22. package/src/engine/template-engine.js +12 -3
  23. package/src/filters/array.js +14 -6
  24. package/src/filters/index.js +165 -17
  25. package/src/filters/rdf.js +3 -3
  26. package/src/{index.js → index.mjs} +46 -0
  27. package/src/index.test.mjs +40 -0
  28. package/src/inheritance/index.js +19 -1
  29. package/src/injection/atomic-writer.js +22 -1
  30. package/src/injection/idempotency-manager.js +33 -0
  31. package/src/injection/injection-engine.js +46 -1
  32. package/src/injection/integration.js +3 -3
  33. package/src/injection/modes/index.js +30 -0
  34. package/src/injection/rollback-manager.js +26 -2
  35. package/src/injection/target-resolver.js +48 -3
  36. package/src/injection/tests/injection-engine.test.js +3 -3
  37. package/src/injection/tests/integration.test.js +2 -1
  38. package/src/injection/tests/run-tests.js +3 -0
  39. package/src/injection/validation-engine.js +71 -5
  40. package/src/linter/determinism-linter.js +20 -5
  41. package/src/linter/determinism.js +8 -2
  42. package/src/linter/index.js +3 -1
  43. package/src/linter/test-doubles.js +151 -4
  44. package/src/parser/frontmatter.js +6 -0
  45. package/src/parser/variables.js +7 -1
  46. package/src/rdf/filters.js +393 -0
  47. package/src/rdf/index.js +444 -0
  48. package/src/renderer/deterministic.js +6 -0
  49. package/src/renderer/index.js +3 -1
  50. package/src/templates/rdf/DELIVERY-SUMMARY.md +266 -0
  51. package/src/templates/rdf/README.md +595 -0
  52. package/src/templates/rdf/dataset.njk +83 -0
  53. package/src/templates/rdf/index.js +106 -0
  54. package/src/templates/rdf/jsonld-context.njk +63 -0
  55. package/src/templates/rdf/ontology.njk +107 -0
  56. package/src/templates/rdf/schema.njk +79 -0
  57. package/src/templates/rdf/shapes.njk +89 -0
  58. package/src/templates/rdf/sparql-queries.njk +70 -0
  59. 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;