@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,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGEN Template Parser - Parse template syntax without nunjucks
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Variables: {{ variable }}
|
|
6
|
+
* - Filters: {{ variable | filter }}
|
|
7
|
+
* - Conditions: {% if condition %}...{% endif %}
|
|
8
|
+
* - Loops: {% for item in items %}...{% endfor %}
|
|
9
|
+
* - Comments: {# comment #}
|
|
10
|
+
* - Frontmatter: YAML header
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Use existing frontmatter parser if available, fallback to basic parsing
|
|
14
|
+
|
|
15
|
+
export class KGenParser {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.options = {
|
|
18
|
+
maxDepth: options.maxDepth || 10,
|
|
19
|
+
enableIncludes: options.enableIncludes !== false,
|
|
20
|
+
strictMode: options.strictMode !== false,
|
|
21
|
+
...options
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Template syntax patterns
|
|
25
|
+
this.patterns = {
|
|
26
|
+
variable: /\{\{\s*([^}]+)\s*\}\}/g,
|
|
27
|
+
expression: /\{\%\s*([^%]+)\s*\%\}/g,
|
|
28
|
+
comment: /\{#\s*([^#]+)\s*#\}/g,
|
|
29
|
+
frontmatter: /^---\n([\s\S]*?)\n---\n([\s\S]*)$/
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse template content into structured format
|
|
35
|
+
*/
|
|
36
|
+
async parse(template) {
|
|
37
|
+
try {
|
|
38
|
+
// Extract frontmatter if present
|
|
39
|
+
const { frontmatter, content } = this.parseFrontmatter(template);
|
|
40
|
+
|
|
41
|
+
// Parse template structure
|
|
42
|
+
const parseResult = {
|
|
43
|
+
template: content,
|
|
44
|
+
frontmatter: frontmatter || {},
|
|
45
|
+
variables: this.extractVariables(content),
|
|
46
|
+
expressions: this.extractExpressions(content),
|
|
47
|
+
includes: this.extractIncludes(content),
|
|
48
|
+
comments: this.extractComments(content),
|
|
49
|
+
structure: this.analyzeStructure(content)
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Validate syntax
|
|
53
|
+
if (this.options.strictMode) {
|
|
54
|
+
this.validateSyntax(parseResult);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parseResult;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`Parse error: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse frontmatter from template
|
|
65
|
+
*/
|
|
66
|
+
parseFrontmatter(template) {
|
|
67
|
+
const match = template.match(this.patterns.frontmatter);
|
|
68
|
+
|
|
69
|
+
if (match) {
|
|
70
|
+
try {
|
|
71
|
+
// Try to parse YAML frontmatter
|
|
72
|
+
const yamlContent = match[1];
|
|
73
|
+
const templateContent = match[2];
|
|
74
|
+
|
|
75
|
+
// Basic YAML parsing (simplified)
|
|
76
|
+
const frontmatter = this.parseBasicYAML(yamlContent);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
frontmatter,
|
|
80
|
+
content: templateContent
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Fallback to basic parsing
|
|
84
|
+
return {
|
|
85
|
+
frontmatter: {},
|
|
86
|
+
content: template
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
frontmatter: {},
|
|
93
|
+
content: template
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Basic YAML parser for frontmatter (simplified)
|
|
99
|
+
*/
|
|
100
|
+
parseBasicYAML(yamlContent) {
|
|
101
|
+
const result = {};
|
|
102
|
+
const lines = yamlContent.split('\n');
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
107
|
+
|
|
108
|
+
const colonIndex = trimmed.indexOf(':');
|
|
109
|
+
if (colonIndex === -1) continue;
|
|
110
|
+
|
|
111
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
112
|
+
let value = trimmed.substring(colonIndex + 1).trim();
|
|
113
|
+
|
|
114
|
+
// Parse basic value types
|
|
115
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
116
|
+
value = value.slice(1, -1);
|
|
117
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
118
|
+
value = value.slice(1, -1);
|
|
119
|
+
} else if (value === 'true') {
|
|
120
|
+
value = true;
|
|
121
|
+
} else if (value === 'false') {
|
|
122
|
+
value = false;
|
|
123
|
+
} else if (value === 'null') {
|
|
124
|
+
value = null;
|
|
125
|
+
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
126
|
+
value = parseFloat(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
result[key] = value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extract variables from template content
|
|
137
|
+
*/
|
|
138
|
+
extractVariables(content) {
|
|
139
|
+
const variables = new Set();
|
|
140
|
+
const optionalVariables = new Set();
|
|
141
|
+
let match;
|
|
142
|
+
|
|
143
|
+
// Reset regex state
|
|
144
|
+
this.patterns.variable.lastIndex = 0;
|
|
145
|
+
|
|
146
|
+
while ((match = this.patterns.variable.exec(content)) !== null) {
|
|
147
|
+
const variableExpr = match[1].trim();
|
|
148
|
+
|
|
149
|
+
// Handle filtered variables: {{ variable | filter }}
|
|
150
|
+
const parts = variableExpr.split('|');
|
|
151
|
+
const [variableName] = parts[0].trim().split('.');
|
|
152
|
+
|
|
153
|
+
// Skip loop variables and built-ins
|
|
154
|
+
if (!this.isBuiltinVariable(variableName)) {
|
|
155
|
+
variables.add(variableName);
|
|
156
|
+
|
|
157
|
+
// Check if variable has default filter (makes it optional)
|
|
158
|
+
if (parts.length > 1) {
|
|
159
|
+
const filters = parts.slice(1);
|
|
160
|
+
for (const filterExpr of filters) {
|
|
161
|
+
const filterName = filterExpr.trim().split(/[\s(]/)[0];
|
|
162
|
+
if (filterName === 'default') {
|
|
163
|
+
optionalVariables.add(variableName);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Return only required variables (excluding optional ones)
|
|
172
|
+
const required = Array.from(variables).filter(v => !optionalVariables.has(v));
|
|
173
|
+
return required;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract expressions (conditionals, loops, etc.)
|
|
178
|
+
*/
|
|
179
|
+
extractExpressions(content) {
|
|
180
|
+
const expressions = [];
|
|
181
|
+
let match;
|
|
182
|
+
|
|
183
|
+
// Reset regex state
|
|
184
|
+
this.patterns.expression.lastIndex = 0;
|
|
185
|
+
|
|
186
|
+
while ((match = this.patterns.expression.exec(content)) !== null) {
|
|
187
|
+
const expr = match[1].trim();
|
|
188
|
+
const type = this.getExpressionType(expr);
|
|
189
|
+
|
|
190
|
+
expressions.push({
|
|
191
|
+
type,
|
|
192
|
+
expression: expr,
|
|
193
|
+
position: match.index,
|
|
194
|
+
raw: match[0]
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return expressions;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Extract include statements
|
|
203
|
+
*/
|
|
204
|
+
extractIncludes(content) {
|
|
205
|
+
const includes = [];
|
|
206
|
+
const expressions = this.extractExpressions(content);
|
|
207
|
+
|
|
208
|
+
expressions.forEach(expr => {
|
|
209
|
+
if (expr.type === 'include') {
|
|
210
|
+
const pathMatch = expr.expression.match(/include\s+['"]([^'"]+)['"]/);
|
|
211
|
+
if (pathMatch) {
|
|
212
|
+
includes.push({
|
|
213
|
+
path: pathMatch[1],
|
|
214
|
+
position: expr.position,
|
|
215
|
+
raw: expr.raw
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return includes;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Extract comments
|
|
226
|
+
*/
|
|
227
|
+
extractComments(content) {
|
|
228
|
+
const comments = [];
|
|
229
|
+
let match;
|
|
230
|
+
|
|
231
|
+
// Reset regex state
|
|
232
|
+
this.patterns.comment.lastIndex = 0;
|
|
233
|
+
|
|
234
|
+
while ((match = this.patterns.comment.exec(content)) !== null) {
|
|
235
|
+
comments.push({
|
|
236
|
+
text: match[1].trim(),
|
|
237
|
+
position: match.index,
|
|
238
|
+
raw: match[0]
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return comments;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Analyze template structure
|
|
247
|
+
*/
|
|
248
|
+
analyzeStructure(content) {
|
|
249
|
+
const expressions = this.extractExpressions(content);
|
|
250
|
+
|
|
251
|
+
const structure = {
|
|
252
|
+
conditionals: 0,
|
|
253
|
+
loops: 0,
|
|
254
|
+
includes: 0,
|
|
255
|
+
blocks: 0,
|
|
256
|
+
macros: 0,
|
|
257
|
+
depth: 0
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const stack = [];
|
|
261
|
+
|
|
262
|
+
expressions.forEach(expr => {
|
|
263
|
+
switch (expr.type) {
|
|
264
|
+
case 'if':
|
|
265
|
+
structure.conditionals++;
|
|
266
|
+
stack.push('if');
|
|
267
|
+
break;
|
|
268
|
+
case 'elif':
|
|
269
|
+
case 'else':
|
|
270
|
+
// Don't increment depth for elif/else
|
|
271
|
+
break;
|
|
272
|
+
case 'endif':
|
|
273
|
+
if (stack.length > 0 && stack[stack.length - 1] === 'if') {
|
|
274
|
+
stack.pop();
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
case 'for':
|
|
278
|
+
structure.loops++;
|
|
279
|
+
stack.push('for');
|
|
280
|
+
break;
|
|
281
|
+
case 'endfor':
|
|
282
|
+
if (stack.length > 0 && stack[stack.length - 1] === 'for') {
|
|
283
|
+
stack.pop();
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
case 'include':
|
|
287
|
+
structure.includes++;
|
|
288
|
+
break;
|
|
289
|
+
case 'block':
|
|
290
|
+
structure.blocks++;
|
|
291
|
+
stack.push('block');
|
|
292
|
+
break;
|
|
293
|
+
case 'endblock':
|
|
294
|
+
if (stack.length > 0 && stack[stack.length - 1] === 'block') {
|
|
295
|
+
stack.pop();
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
case 'macro':
|
|
299
|
+
structure.macros++;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Track maximum nesting depth
|
|
304
|
+
structure.depth = Math.max(structure.depth, stack.length);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return structure;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get expression type from expression string
|
|
312
|
+
*/
|
|
313
|
+
getExpressionType(expr) {
|
|
314
|
+
const trimmed = expr.trim().toLowerCase();
|
|
315
|
+
|
|
316
|
+
if (trimmed.startsWith('if ')) return 'if';
|
|
317
|
+
if (trimmed === 'else') return 'else';
|
|
318
|
+
if (trimmed.startsWith('elif ')) return 'elif';
|
|
319
|
+
if (trimmed === 'endif') return 'endif';
|
|
320
|
+
if (trimmed.startsWith('for ')) return 'for';
|
|
321
|
+
if (trimmed === 'endfor') return 'endfor';
|
|
322
|
+
if (trimmed.startsWith('include ')) return 'include';
|
|
323
|
+
if (trimmed.startsWith('block ')) return 'block';
|
|
324
|
+
if (trimmed === 'endblock') return 'endblock';
|
|
325
|
+
if (trimmed.startsWith('macro ')) return 'macro';
|
|
326
|
+
if (trimmed === 'endmacro') return 'endmacro';
|
|
327
|
+
if (trimmed.startsWith('set ')) return 'set';
|
|
328
|
+
|
|
329
|
+
return 'unknown';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if variable is a built-in
|
|
334
|
+
*/
|
|
335
|
+
isBuiltinVariable(name) {
|
|
336
|
+
const builtins = new Set([
|
|
337
|
+
'loop', 'super', '__kgen', 'true', 'false', 'null', 'undefined'
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
return builtins.has(name) || name.startsWith('__');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Validate template syntax
|
|
345
|
+
*/
|
|
346
|
+
validateSyntax(parseResult) {
|
|
347
|
+
const { expressions } = parseResult;
|
|
348
|
+
const errors = [];
|
|
349
|
+
const stack = [];
|
|
350
|
+
|
|
351
|
+
// Check for balanced expressions
|
|
352
|
+
expressions.forEach((expr, index) => {
|
|
353
|
+
switch (expr.type) {
|
|
354
|
+
case 'if':
|
|
355
|
+
stack.push({ type: 'if', index });
|
|
356
|
+
break;
|
|
357
|
+
case 'endif':
|
|
358
|
+
if (stack.length === 0 || stack[stack.length - 1].type !== 'if') {
|
|
359
|
+
errors.push(`Unmatched {% endif %} at position ${expr.position}`);
|
|
360
|
+
} else {
|
|
361
|
+
stack.pop();
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
case 'for':
|
|
365
|
+
stack.push({ type: 'for', index });
|
|
366
|
+
break;
|
|
367
|
+
case 'endfor':
|
|
368
|
+
if (stack.length === 0 || stack[stack.length - 1].type !== 'for') {
|
|
369
|
+
errors.push(`Unmatched {% endfor %} at position ${expr.position}`);
|
|
370
|
+
} else {
|
|
371
|
+
stack.pop();
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
case 'block':
|
|
375
|
+
stack.push({ type: 'block', index });
|
|
376
|
+
break;
|
|
377
|
+
case 'endblock':
|
|
378
|
+
if (stack.length === 0 || stack[stack.length - 1].type !== 'block') {
|
|
379
|
+
errors.push(`Unmatched {% endblock %} at position ${expr.position}`);
|
|
380
|
+
} else {
|
|
381
|
+
stack.pop();
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Check for unclosed expressions
|
|
388
|
+
stack.forEach(unclosed => {
|
|
389
|
+
const expr = expressions[unclosed.index];
|
|
390
|
+
errors.push(`Unclosed {% ${expr.type} %} at position ${expr.position}`);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Check depth limits
|
|
394
|
+
if (parseResult.structure.depth > this.options.maxDepth) {
|
|
395
|
+
errors.push(`Template nesting depth ${parseResult.structure.depth} exceeds maximum ${this.options.maxDepth}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (errors.length > 0) {
|
|
399
|
+
throw new Error(`Syntax validation failed:\n${errors.join('\n')}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Check if template is deterministic
|
|
405
|
+
*/
|
|
406
|
+
isDeterministic(parseResult) {
|
|
407
|
+
const { variables, expressions } = parseResult;
|
|
408
|
+
|
|
409
|
+
// Check for non-deterministic variables
|
|
410
|
+
const nonDeterministicVars = variables.filter(name =>
|
|
411
|
+
['now', 'random', 'uuid'].includes(name)
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (nonDeterministicVars.length > 0) {
|
|
415
|
+
return {
|
|
416
|
+
deterministic: false,
|
|
417
|
+
reasons: [`Non-deterministic variables: ${nonDeterministicVars.join(', ')}`]
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Check for non-deterministic expressions
|
|
422
|
+
const nonDeterministicExpressions = expressions.filter(expr =>
|
|
423
|
+
/\b(now|random|uuid)\b/.test(expr.expression)
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
if (nonDeterministicExpressions.length > 0) {
|
|
427
|
+
return {
|
|
428
|
+
deterministic: false,
|
|
429
|
+
reasons: [`Non-deterministic expressions found`]
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return { deterministic: true, reasons: [] };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get parser statistics
|
|
438
|
+
*/
|
|
439
|
+
getStats() {
|
|
440
|
+
return {
|
|
441
|
+
...this.options,
|
|
442
|
+
supportedPatterns: Object.keys(this.patterns)
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export default KGenParser;
|