@unrdf/kgn 5.0.1
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/LICENSE +21 -0
- package/README.md +210 -0
- package/package.json +90 -0
- package/src/MIGRATION_COMPLETE.md +186 -0
- package/src/PORT-MAP.md +302 -0
- package/src/base/filter-templates.js +479 -0
- package/src/base/index.js +92 -0
- package/src/base/injection-targets.js +583 -0
- package/src/base/macro-templates.js +298 -0
- package/src/base/macro-templates.js.bak +461 -0
- package/src/base/shacl-templates.js +617 -0
- package/src/base/template-base.js +388 -0
- package/src/core/attestor.js +381 -0
- package/src/core/filters.js +518 -0
- package/src/core/index.js +21 -0
- package/src/core/kgen-engine.js +372 -0
- package/src/core/parser.js +447 -0
- package/src/core/post-processor.js +313 -0
- package/src/core/renderer.js +469 -0
- package/src/doc-generator/cli.mjs +122 -0
- package/src/doc-generator/index.mjs +28 -0
- package/src/doc-generator/mdx-generator.mjs +71 -0
- package/src/doc-generator/nav-generator.mjs +136 -0
- package/src/doc-generator/parser.mjs +291 -0
- package/src/doc-generator/rdf-builder.mjs +306 -0
- package/src/doc-generator/scanner.mjs +189 -0
- package/src/engine/index.js +42 -0
- package/src/engine/pipeline.js +448 -0
- package/src/engine/renderer.js +604 -0
- package/src/engine/template-engine.js +566 -0
- package/src/filters/array.js +436 -0
- package/src/filters/data.js +479 -0
- package/src/filters/index.js +270 -0
- package/src/filters/rdf.js +264 -0
- package/src/filters/text.js +369 -0
- package/src/index.js +109 -0
- package/src/inheritance/index.js +40 -0
- package/src/injection/api.js +260 -0
- package/src/injection/atomic-writer.js +327 -0
- package/src/injection/constants.js +136 -0
- package/src/injection/idempotency-manager.js +295 -0
- package/src/injection/index.js +28 -0
- package/src/injection/injection-engine.js +378 -0
- package/src/injection/integration.js +339 -0
- package/src/injection/modes/index.js +341 -0
- package/src/injection/rollback-manager.js +373 -0
- package/src/injection/target-resolver.js +323 -0
- package/src/injection/tests/atomic-writer.test.js +382 -0
- package/src/injection/tests/injection-engine.test.js +611 -0
- package/src/injection/tests/integration.test.js +392 -0
- package/src/injection/tests/run-tests.js +283 -0
- package/src/injection/validation-engine.js +547 -0
- package/src/linter/determinism-linter.js +473 -0
- package/src/linter/determinism.js +410 -0
- package/src/linter/index.js +6 -0
- package/src/linter/test-doubles.js +475 -0
- package/src/parser/frontmatter.js +228 -0
- package/src/parser/variables.js +344 -0
- package/src/renderer/deterministic.js +245 -0
- package/src/renderer/index.js +6 -0
- package/src/templates/latex/academic-paper.njk +186 -0
- package/src/templates/latex/index.js +104 -0
- package/src/templates/nextjs/app-page.njk +66 -0
- package/src/templates/nextjs/index.js +80 -0
- package/src/templates/office/docx/document.njk +368 -0
- package/src/templates/office/index.js +79 -0
- package/src/templates/office/word-report.njk +129 -0
- package/src/utils/template-utils.js +426 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGEN Template Filters - Complete Filter Library
|
|
3
|
+
*
|
|
4
|
+
* Migrated essential filters from unjucks with 80/20 principle:
|
|
5
|
+
* - Text processing (upper, lower, camelCase, etc.)
|
|
6
|
+
* - Array manipulation (join, sort, unique, etc.)
|
|
7
|
+
* - Data handling (json, default, type checking)
|
|
8
|
+
* - RDF/SPARQL support (mocked for deterministic testing)
|
|
9
|
+
*
|
|
10
|
+
* All filters provide deterministic, reproducible output
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import { textFilters } from './text.js';
|
|
15
|
+
import { arrayFilters } from './array.js';
|
|
16
|
+
import { dataFilters } from './data.js';
|
|
17
|
+
import { rdfFilters } from './rdf.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create custom filters with deterministic operations
|
|
21
|
+
*/
|
|
22
|
+
export function createCustomFilters(options = {}) {
|
|
23
|
+
const { deterministicMode = false, staticBuildTime = '2024-01-01T00:00:00.000Z' } = options;
|
|
24
|
+
|
|
25
|
+
// Merge all filter collections
|
|
26
|
+
const allFilters = {
|
|
27
|
+
...textFilters,
|
|
28
|
+
...arrayFilters,
|
|
29
|
+
...dataFilters,
|
|
30
|
+
...rdfFilters,
|
|
31
|
+
|
|
32
|
+
// Enhanced deterministic filters that override or augment the base filters
|
|
33
|
+
|
|
34
|
+
// Deterministic date/time formatting
|
|
35
|
+
formatDate: (date, format = 'YYYY-MM-DD') => {
|
|
36
|
+
if (deterministicMode) {
|
|
37
|
+
// Use static build time for deterministic rendering
|
|
38
|
+
return staticBuildTime.split('T')[0]; // Returns '2024-01-01'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const d = new Date(date);
|
|
42
|
+
if (isNaN(d.getTime())) return '';
|
|
43
|
+
|
|
44
|
+
switch (format) {
|
|
45
|
+
case 'YYYY-MM-DD':
|
|
46
|
+
return d.toISOString().split('T')[0];
|
|
47
|
+
case 'MM/DD/YYYY':
|
|
48
|
+
return `${(d.getMonth() + 1).toString().padStart(2, '0')}/${d.getDate().toString().padStart(2, '0')}/${d.getFullYear()}`;
|
|
49
|
+
case 'DD/MM/YYYY':
|
|
50
|
+
return `${d.getDate().toString().padStart(2, '0')}/${(d.getMonth() + 1).toString().padStart(2, '0')}/${d.getFullYear()}`;
|
|
51
|
+
default:
|
|
52
|
+
return d.toISOString().split('T')[0];
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
formatTime: (date, format = 'HH:mm:ss') => {
|
|
57
|
+
if (deterministicMode) {
|
|
58
|
+
return '00:00:00';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const d = new Date(date);
|
|
62
|
+
if (isNaN(d.getTime())) return '';
|
|
63
|
+
|
|
64
|
+
const hours = d.getHours().toString().padStart(2, '0');
|
|
65
|
+
const minutes = d.getMinutes().toString().padStart(2, '0');
|
|
66
|
+
const seconds = d.getSeconds().toString().padStart(2, '0');
|
|
67
|
+
|
|
68
|
+
switch (format) {
|
|
69
|
+
case 'HH:mm:ss':
|
|
70
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
71
|
+
case 'HH:mm':
|
|
72
|
+
return `${hours}:${minutes}`;
|
|
73
|
+
default:
|
|
74
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Deterministic timestamp
|
|
79
|
+
timestamp: () => {
|
|
80
|
+
if (deterministicMode) {
|
|
81
|
+
return staticBuildTime;
|
|
82
|
+
}
|
|
83
|
+
return new Date().toISOString();
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Content hashing for deterministic IDs
|
|
87
|
+
hash: (content, algorithm = 'sha256') => {
|
|
88
|
+
return crypto.createHash(algorithm).update(String(content)).digest('hex');
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
shortHash: (content, length = 8) => {
|
|
92
|
+
return crypto.createHash('sha256').update(String(content)).digest('hex').substring(0, length);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// File and path utilities
|
|
96
|
+
filename: (filePath) => {
|
|
97
|
+
if (!filePath) return '';
|
|
98
|
+
return filePath.split('/').pop().split('\\').pop();
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
basename: (filePath, ext) => {
|
|
102
|
+
const name = filePath.split('/').pop().split('\\').pop();
|
|
103
|
+
if (ext && name.endsWith(ext)) {
|
|
104
|
+
return name.slice(0, -ext.length);
|
|
105
|
+
}
|
|
106
|
+
const dotIndex = name.lastIndexOf('.');
|
|
107
|
+
return dotIndex > 0 ? name.slice(0, dotIndex) : name;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
dirname: (filePath) => {
|
|
111
|
+
if (!filePath) return '';
|
|
112
|
+
const parts = filePath.split('/');
|
|
113
|
+
if (parts.length === 1) {
|
|
114
|
+
// Try Windows separators
|
|
115
|
+
const winParts = filePath.split('\\');
|
|
116
|
+
return winParts.length > 1 ? winParts.slice(0, -1).join('\\') : '';
|
|
117
|
+
}
|
|
118
|
+
return parts.slice(0, -1).join('/');
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Code generation helpers
|
|
122
|
+
comment: (text, style = '//') => {
|
|
123
|
+
if (!text) return '';
|
|
124
|
+
switch (style) {
|
|
125
|
+
case '//':
|
|
126
|
+
return text.split('\n').map(line => `// ${line}`).join('\n');
|
|
127
|
+
case '#':
|
|
128
|
+
return text.split('\n').map(line => `# ${line}`).join('\n');
|
|
129
|
+
case '/*':
|
|
130
|
+
return `/* ${text} */`;
|
|
131
|
+
case '<!--':
|
|
132
|
+
return `<!-- ${text} -->`;
|
|
133
|
+
default:
|
|
134
|
+
return text.split('\n').map(line => `${style} ${line}`).join('\n');
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// Validation and safety filters
|
|
139
|
+
required: (value, message = 'Value is required') => {
|
|
140
|
+
if (value === null || value === undefined || value === '') {
|
|
141
|
+
throw new Error(message);
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Determinism blockers - these will throw in deterministic mode
|
|
147
|
+
now: () => {
|
|
148
|
+
if (deterministicMode) {
|
|
149
|
+
throw new Error('Filter "now" is not allowed in deterministic mode. Use "timestamp" instead.');
|
|
150
|
+
}
|
|
151
|
+
return new Date().toISOString();
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
random: () => {
|
|
155
|
+
if (deterministicMode) {
|
|
156
|
+
throw new Error('Filter "random" is not allowed in deterministic mode. Use "hash" for consistent randomness.');
|
|
157
|
+
}
|
|
158
|
+
return Math.random();
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
uuid: () => {
|
|
162
|
+
if (deterministicMode) {
|
|
163
|
+
throw new Error('Filter "uuid" is not allowed in deterministic mode. Use "hash" for consistent IDs.');
|
|
164
|
+
}
|
|
165
|
+
return crypto.randomUUID();
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// CSV conversion filters
|
|
169
|
+
csv: (data, options = {}) => {
|
|
170
|
+
if (!Array.isArray(data)) return '';
|
|
171
|
+
if (data.length === 0) return '';
|
|
172
|
+
|
|
173
|
+
const { delimiter = ',', quote = '"', headers = true } = options;
|
|
174
|
+
const lines = [];
|
|
175
|
+
|
|
176
|
+
// Get headers from first object
|
|
177
|
+
if (headers && data.length > 0 && typeof data[0] === 'object') {
|
|
178
|
+
const headerRow = Object.keys(data[0]).map(h => `${quote}${h}${quote}`).join(delimiter);
|
|
179
|
+
lines.push(headerRow);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add data rows
|
|
183
|
+
for (const row of data) {
|
|
184
|
+
if (typeof row === 'object' && row !== null) {
|
|
185
|
+
const values = Object.values(row).map(v => {
|
|
186
|
+
const str = String(v || '');
|
|
187
|
+
return str.includes(delimiter) || str.includes(quote) || str.includes('\n')
|
|
188
|
+
? `${quote}${str.replace(new RegExp(quote, 'g'), quote + quote)}${quote}`
|
|
189
|
+
: str;
|
|
190
|
+
});
|
|
191
|
+
lines.push(values.join(delimiter));
|
|
192
|
+
} else {
|
|
193
|
+
lines.push(String(row));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return lines.join('\n');
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// Markdown table filter
|
|
201
|
+
markdown: (data, options = {}) => {
|
|
202
|
+
if (!Array.isArray(data)) return '';
|
|
203
|
+
if (data.length === 0) return '';
|
|
204
|
+
|
|
205
|
+
const { align = 'left' } = options;
|
|
206
|
+
const lines = [];
|
|
207
|
+
|
|
208
|
+
if (typeof data[0] === 'object' && data[0] !== null) {
|
|
209
|
+
const headers = Object.keys(data[0]);
|
|
210
|
+
|
|
211
|
+
// Header row
|
|
212
|
+
lines.push('| ' + headers.join(' | ') + ' |');
|
|
213
|
+
|
|
214
|
+
// Separator row
|
|
215
|
+
const separators = headers.map(() => {
|
|
216
|
+
switch (align) {
|
|
217
|
+
case 'center': return ':---:';
|
|
218
|
+
case 'right': return '---:';
|
|
219
|
+
default: return '---';
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
lines.push('| ' + separators.join(' | ') + ' |');
|
|
223
|
+
|
|
224
|
+
// Data rows
|
|
225
|
+
for (const row of data) {
|
|
226
|
+
const values = headers.map(h => String(row[h] || '').replace(/\|/g, '\\|'));
|
|
227
|
+
lines.push('| ' + values.join(' | ') + ' |');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return allFilters;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Register all filters with Nunjucks environment
|
|
240
|
+
* @param {Object} env - Nunjucks environment
|
|
241
|
+
* @param {Object} options - Filter options
|
|
242
|
+
*/
|
|
243
|
+
export function registerFilters(env, options = {}) {
|
|
244
|
+
const filters = createCustomFilters(options);
|
|
245
|
+
|
|
246
|
+
for (const [name, filter] of Object.entries(filters)) {
|
|
247
|
+
env.addFilter(name, filter);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return env;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get all available filter names
|
|
255
|
+
* @param {Object} options - Filter options
|
|
256
|
+
* @returns {Array} Array of filter names
|
|
257
|
+
*/
|
|
258
|
+
export function getFilterNames(options = {}) {
|
|
259
|
+
return Object.keys(createCustomFilters(options));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Export individual filter collections
|
|
264
|
+
*/
|
|
265
|
+
export { textFilters, arrayFilters, dataFilters, rdfFilters };
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Export default function
|
|
269
|
+
*/
|
|
270
|
+
export default createCustomFilters;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGEN RDF/SPARQL Filters - P0 Essential Semantic Web Processing
|
|
3
|
+
*
|
|
4
|
+
* Mock implementations for testing and London BDD approach
|
|
5
|
+
* These stubs provide deterministic behavior for template generation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mock RDF prefixes for expansion
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_PREFIXES = {
|
|
12
|
+
'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
13
|
+
'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
|
|
14
|
+
'owl': 'http://www.w3.org/2002/07/owl#',
|
|
15
|
+
'xsd': 'http://www.w3.org/2001/XMLSchema#',
|
|
16
|
+
'dc': 'http://purl.org/dc/elements/1.1/',
|
|
17
|
+
'foaf': 'http://xmlns.com/foaf/0.1/',
|
|
18
|
+
'skos': 'http://www.w3.org/2004/02/skos/core#',
|
|
19
|
+
'ex': 'http://example.org/',
|
|
20
|
+
'schema': 'https://schema.org/'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Mock SPARQL dataset for deterministic results
|
|
25
|
+
*/
|
|
26
|
+
const MOCK_SPARQL_RESULTS = {
|
|
27
|
+
'SELECT ?s ?p ?o WHERE { ?s ?p ?o }': [
|
|
28
|
+
{ s: 'ex:Subject1', p: 'rdf:type', o: 'ex:Class1' },
|
|
29
|
+
{ s: 'ex:Subject2', p: 'rdfs:label', o: '"Example Label"' }
|
|
30
|
+
],
|
|
31
|
+
'SELECT * WHERE { ?s rdf:type ?type }': [
|
|
32
|
+
{ s: 'ex:Instance1', type: 'ex:Person' },
|
|
33
|
+
{ s: 'ex:Instance2', type: 'ex:Organization' }
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Expand CURIE (Compact URI) to full URI
|
|
39
|
+
* @param {string} curie - CURIE in format 'prefix:localName'
|
|
40
|
+
* @param {Object} prefixes - Custom prefix mappings
|
|
41
|
+
* @returns {string} Expanded URI
|
|
42
|
+
*/
|
|
43
|
+
export const expand = (curie, prefixes = {}) => {
|
|
44
|
+
if (!curie || typeof curie !== 'string') return '';
|
|
45
|
+
|
|
46
|
+
const allPrefixes = { ...DEFAULT_PREFIXES, ...prefixes };
|
|
47
|
+
const [prefix, localName] = curie.split(':', 2);
|
|
48
|
+
|
|
49
|
+
if (localName === undefined) return curie; // No prefix found
|
|
50
|
+
if (!allPrefixes[prefix]) return curie; // Unknown prefix
|
|
51
|
+
|
|
52
|
+
return allPrefixes[prefix] + localName;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Contract full URI to CURIE if possible
|
|
57
|
+
* @param {string} uri - Full URI
|
|
58
|
+
* @param {Object} prefixes - Custom prefix mappings
|
|
59
|
+
* @returns {string} CURIE or original URI
|
|
60
|
+
*/
|
|
61
|
+
export const contract = (uri, prefixes = {}) => {
|
|
62
|
+
if (!uri || typeof uri !== 'string') return '';
|
|
63
|
+
|
|
64
|
+
const allPrefixes = { ...DEFAULT_PREFIXES, ...prefixes };
|
|
65
|
+
|
|
66
|
+
for (const [prefix, namespace] of Object.entries(allPrefixes)) {
|
|
67
|
+
if (uri.startsWith(namespace)) {
|
|
68
|
+
const localName = uri.substring(namespace.length);
|
|
69
|
+
return `${prefix}:${localName}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return uri;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute mock SPARQL query (deterministic results)
|
|
78
|
+
* @param {string} query - SPARQL query string
|
|
79
|
+
* @param {any} dataset - Mock dataset (ignored in stub)
|
|
80
|
+
* @returns {Array} Mock query results
|
|
81
|
+
*/
|
|
82
|
+
export const sparql = async (query, dataset = null) => {
|
|
83
|
+
if (!query || typeof query !== 'string') return [];
|
|
84
|
+
|
|
85
|
+
// Return mock results for known queries
|
|
86
|
+
const normalizedQuery = query.trim().replace(/\s+/g, ' ');
|
|
87
|
+
|
|
88
|
+
if (MOCK_SPARQL_RESULTS[normalizedQuery]) {
|
|
89
|
+
return MOCK_SPARQL_RESULTS[normalizedQuery];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Generate deterministic mock results based on query hash
|
|
93
|
+
const queryHash = query.split('').reduce((hash, char) => {
|
|
94
|
+
hash = ((hash << 5) - hash) + char.charCodeAt(0);
|
|
95
|
+
return hash & hash;
|
|
96
|
+
}, 0);
|
|
97
|
+
|
|
98
|
+
const numResults = Math.abs(queryHash) % 5 + 1; // 1-5 results
|
|
99
|
+
const results = [];
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < numResults; i++) {
|
|
102
|
+
results.push({
|
|
103
|
+
s: `ex:MockSubject${i + 1}`,
|
|
104
|
+
p: `ex:mockProperty${i + 1}`,
|
|
105
|
+
o: `"Mock Object ${i + 1}"`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return results;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create RDF literal with optional language tag or datatype
|
|
114
|
+
* @param {any} value - Literal value
|
|
115
|
+
* @param {string} langOrType - Language tag (e.g., 'en') or datatype CURIE
|
|
116
|
+
* @returns {string} RDF literal string
|
|
117
|
+
*/
|
|
118
|
+
export const rdfLiteral = (value, langOrType = null) => {
|
|
119
|
+
if (value === null || value === undefined) return '""';
|
|
120
|
+
|
|
121
|
+
const literal = `"${String(value).replace(/"/g, '\\"')}"`;
|
|
122
|
+
|
|
123
|
+
if (!langOrType) return literal;
|
|
124
|
+
|
|
125
|
+
// Check if it's a language tag (2-3 letter code)
|
|
126
|
+
if (/^[a-z]{2,3}(-[A-Z]{2})?$/.test(langOrType)) {
|
|
127
|
+
return `${literal}@${langOrType}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Treat as datatype
|
|
131
|
+
return `${literal}^^${langOrType}`;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create RDF resource (URI reference)
|
|
136
|
+
* @param {string} uri - URI string
|
|
137
|
+
* @returns {string} URI wrapped in angle brackets
|
|
138
|
+
*/
|
|
139
|
+
export const rdfResource = (uri) => {
|
|
140
|
+
if (!uri || typeof uri !== 'string') return '<>';
|
|
141
|
+
|
|
142
|
+
// If already a CURIE or angle-bracketed URI, return as-is
|
|
143
|
+
if (uri.includes(':') && !uri.startsWith('http')) return uri;
|
|
144
|
+
if (uri.startsWith('<') && uri.endsWith('>')) return uri;
|
|
145
|
+
|
|
146
|
+
return `<${uri}>`;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Escape string for Turtle/N3 syntax
|
|
151
|
+
* @param {string} str - String to escape
|
|
152
|
+
* @returns {string} Turtle-escaped string
|
|
153
|
+
*/
|
|
154
|
+
export const turtleEscape = (str) => {
|
|
155
|
+
if (!str || typeof str !== 'string') return '';
|
|
156
|
+
|
|
157
|
+
return str
|
|
158
|
+
.replace(/\\/g, '\\\\')
|
|
159
|
+
.replace(/"/g, '\\"')
|
|
160
|
+
.replace(/\n/g, '\\n')
|
|
161
|
+
.replace(/\r/g, '\\r')
|
|
162
|
+
.replace(/\t/g, '\\t');
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create SPARQL variable
|
|
167
|
+
* @param {string} name - Variable name
|
|
168
|
+
* @returns {string} SPARQL variable with ? prefix
|
|
169
|
+
*/
|
|
170
|
+
export const sparqlVar = (name) => {
|
|
171
|
+
if (!name || typeof name !== 'string') return '?var';
|
|
172
|
+
|
|
173
|
+
// Remove any existing ? or $ prefix
|
|
174
|
+
const cleanName = name.replace(/^[?$]/, '');
|
|
175
|
+
|
|
176
|
+
// Ensure valid SPARQL variable name (alphanumeric + underscore)
|
|
177
|
+
const validName = cleanName.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
178
|
+
|
|
179
|
+
return `?${validName}`;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create RDF list (collection)
|
|
184
|
+
* @param {Array} items - Array items
|
|
185
|
+
* @returns {string} RDF list syntax
|
|
186
|
+
*/
|
|
187
|
+
export const rdfList = (items) => {
|
|
188
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
189
|
+
return 'rdf:nil';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return `( ${items.join(' ')} )`;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create blank node identifier
|
|
197
|
+
* @param {string} id - Optional identifier
|
|
198
|
+
* @returns {string} Blank node with _: prefix
|
|
199
|
+
*/
|
|
200
|
+
export const blankNode = (id = null) => {
|
|
201
|
+
if (id && typeof id === 'string') {
|
|
202
|
+
const cleanId = id.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
203
|
+
return `_:${cleanId}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Generate deterministic blank node ID
|
|
207
|
+
const timestamp = new Date('2024-01-01T00:00:00.000Z').getTime();
|
|
208
|
+
return `_:blank${timestamp % 10000}`;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Add RDF datatype to literal
|
|
213
|
+
* @param {any} value - Value to type
|
|
214
|
+
* @param {string} datatype - XSD datatype (without prefix)
|
|
215
|
+
* @returns {string} Typed literal
|
|
216
|
+
*/
|
|
217
|
+
export const rdfDatatype = (value, datatype = 'string') => {
|
|
218
|
+
if (value === null || value === undefined) return '""';
|
|
219
|
+
|
|
220
|
+
const literal = `"${String(value)}"`;
|
|
221
|
+
return `${literal}^^xsd:${datatype}`;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Mock SHACL validation (always passes in stub)
|
|
226
|
+
* @param {any} data - Data to validate
|
|
227
|
+
* @param {any} shape - SHACL shape
|
|
228
|
+
* @returns {Object} Mock validation result
|
|
229
|
+
*/
|
|
230
|
+
export const shaclValidate = (data, shape) => {
|
|
231
|
+
return {
|
|
232
|
+
conforms: true,
|
|
233
|
+
results: [],
|
|
234
|
+
message: 'Mock SHACL validation passed'
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Mock reasoning/inference (no-op in stub)
|
|
240
|
+
* @param {any} graph - RDF graph
|
|
241
|
+
* @param {string} reasoner - Reasoner type
|
|
242
|
+
* @returns {any} Original graph (no inference applied)
|
|
243
|
+
*/
|
|
244
|
+
export const infer = (graph, reasoner = 'rdfs') => {
|
|
245
|
+
return graph; // No-op for stub
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Collection of all RDF filters for easy import
|
|
249
|
+
export const rdfFilters = {
|
|
250
|
+
expand,
|
|
251
|
+
contract,
|
|
252
|
+
sparql,
|
|
253
|
+
rdfLiteral,
|
|
254
|
+
rdfResource,
|
|
255
|
+
turtleEscape,
|
|
256
|
+
sparqlVar,
|
|
257
|
+
rdfList,
|
|
258
|
+
blankNode,
|
|
259
|
+
rdfDatatype,
|
|
260
|
+
shaclValidate,
|
|
261
|
+
infer
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export default rdfFilters;
|