@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,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGEN Template Renderer - Execute template logic without nunjucks
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Variable interpolation: {{ variable }}
|
|
6
|
+
* - Filter application: {{ variable | filter }}
|
|
7
|
+
* - Conditional rendering: {% if %}...{% endif %}
|
|
8
|
+
* - Loop rendering: {% for %}...{% endfor %}
|
|
9
|
+
* - Include processing: {% include %}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class KGenRenderer {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = {
|
|
15
|
+
maxDepth: options.maxDepth || 10,
|
|
16
|
+
enableIncludes: options.enableIncludes !== false,
|
|
17
|
+
strictMode: options.strictMode !== false,
|
|
18
|
+
deterministicMode: options.deterministicMode !== false,
|
|
19
|
+
...options
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Rendering patterns
|
|
23
|
+
this.patterns = {
|
|
24
|
+
variable: /\{\{\s*([^}]+)\s*\}\}/g,
|
|
25
|
+
expression: /\{\%\s*([^%]+)\s*\%\}/g,
|
|
26
|
+
comment: /\{#\s*([^#]+)\s*#\}/g
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
this.renderDepth = 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Render template with context
|
|
34
|
+
*/
|
|
35
|
+
async render(template, context, options = {}) {
|
|
36
|
+
this.renderDepth = 0;
|
|
37
|
+
|
|
38
|
+
const { filters } = options;
|
|
39
|
+
|
|
40
|
+
if (!filters) {
|
|
41
|
+
throw new Error('Filters instance required for rendering');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await this.processTemplate(template, context, filters);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
content: result,
|
|
49
|
+
metadata: {
|
|
50
|
+
renderTime: this.options.deterministicMode ? '2024-01-01T00:00:00.000Z' : new Date().toISOString(),
|
|
51
|
+
maxDepthReached: this.renderDepth,
|
|
52
|
+
deterministicMode: this.options.deterministicMode
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`Rendering failed: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Process template content recursively
|
|
62
|
+
*/
|
|
63
|
+
async processTemplate(template, context, filters, depth = 0) {
|
|
64
|
+
if (depth > this.options.maxDepth) {
|
|
65
|
+
throw new Error(`Maximum rendering depth ${this.options.maxDepth} exceeded`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.renderDepth = Math.max(this.renderDepth, depth);
|
|
69
|
+
|
|
70
|
+
let processed = template;
|
|
71
|
+
|
|
72
|
+
// Remove comments first
|
|
73
|
+
processed = this.removeComments(processed);
|
|
74
|
+
|
|
75
|
+
// Process expressions (conditionals, loops, etc.)
|
|
76
|
+
processed = await this.processExpressions(processed, context, filters, depth);
|
|
77
|
+
|
|
78
|
+
// Process variables and filters
|
|
79
|
+
processed = this.processVariables(processed, context, filters);
|
|
80
|
+
|
|
81
|
+
return processed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Remove template comments
|
|
86
|
+
*/
|
|
87
|
+
removeComments(template) {
|
|
88
|
+
return template.replace(this.patterns.comment, '');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Process template expressions (if, for, etc.)
|
|
93
|
+
*/
|
|
94
|
+
async processExpressions(template, context, filters, depth) {
|
|
95
|
+
let processed = template;
|
|
96
|
+
|
|
97
|
+
// Process conditionals
|
|
98
|
+
processed = await this.processConditionals(processed, context, filters, depth);
|
|
99
|
+
|
|
100
|
+
// Process loops
|
|
101
|
+
processed = await this.processLoops(processed, context, filters, depth);
|
|
102
|
+
|
|
103
|
+
// Process includes
|
|
104
|
+
if (this.options.enableIncludes) {
|
|
105
|
+
processed = await this.processIncludes(processed, context, filters, depth);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return processed;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Process conditional expressions
|
|
113
|
+
*/
|
|
114
|
+
async processConditionals(template, context, filters, depth) {
|
|
115
|
+
const ifPattern = /\{\%\s*if\s+([^%]+)\s*\%\}([\s\S]*?)(\{\%\s*else\s*\%\}([\s\S]*?))?\{\%\s*endif\s*\%\}/g;
|
|
116
|
+
|
|
117
|
+
let match;
|
|
118
|
+
let processed = template;
|
|
119
|
+
|
|
120
|
+
// Process from end to start to avoid position shifts
|
|
121
|
+
const matches = [];
|
|
122
|
+
while ((match = ifPattern.exec(template)) !== null) {
|
|
123
|
+
matches.push(match);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
127
|
+
match = matches[i];
|
|
128
|
+
const [fullMatch, condition, ifContent, elseBlock, elseContent] = match;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const conditionResult = this.evaluateCondition(condition.trim(), context);
|
|
132
|
+
let replacement;
|
|
133
|
+
|
|
134
|
+
if (conditionResult) {
|
|
135
|
+
replacement = await this.processTemplate(ifContent, context, filters, depth + 1);
|
|
136
|
+
} else if (elseContent !== undefined) {
|
|
137
|
+
replacement = await this.processTemplate(elseContent, context, filters, depth + 1);
|
|
138
|
+
} else {
|
|
139
|
+
replacement = '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
processed = processed.substring(0, match.index) + replacement + processed.substring(match.index + fullMatch.length);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (this.options.strictMode) {
|
|
145
|
+
throw new Error(`Conditional evaluation failed: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
// Replace with empty string in non-strict mode
|
|
148
|
+
const replacement = '';
|
|
149
|
+
processed = processed.substring(0, match.index) + replacement + processed.substring(match.index + fullMatch.length);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return processed;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Process loop expressions
|
|
158
|
+
*/
|
|
159
|
+
async processLoops(template, context, filters, depth) {
|
|
160
|
+
const forPattern = /\{\%\s*for\s+(\w+)\s+in\s+([^%]+)\s*\%\}([\s\S]*?)\{\%\s*endfor\s*\%\}/g;
|
|
161
|
+
|
|
162
|
+
let match;
|
|
163
|
+
let processed = template;
|
|
164
|
+
|
|
165
|
+
// Process from end to start to avoid position shifts
|
|
166
|
+
const matches = [];
|
|
167
|
+
while ((match = forPattern.exec(template)) !== null) {
|
|
168
|
+
matches.push(match);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
172
|
+
match = matches[i];
|
|
173
|
+
const [fullMatch, itemVar, arrayExpr, loopContent] = match;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const array = this.evaluateExpression(arrayExpr.trim(), context);
|
|
177
|
+
let replacement = '';
|
|
178
|
+
|
|
179
|
+
if (Array.isArray(array)) {
|
|
180
|
+
for (let index = 0; index < array.length; index++) {
|
|
181
|
+
const item = array[index];
|
|
182
|
+
|
|
183
|
+
// Create loop context
|
|
184
|
+
const loopContext = {
|
|
185
|
+
...context,
|
|
186
|
+
[itemVar]: item,
|
|
187
|
+
loop: {
|
|
188
|
+
index: index,
|
|
189
|
+
index0: index,
|
|
190
|
+
index1: index + 1,
|
|
191
|
+
first: index === 0,
|
|
192
|
+
last: index === array.length - 1,
|
|
193
|
+
length: array.length,
|
|
194
|
+
revindex: array.length - index,
|
|
195
|
+
revindex0: array.length - index - 1
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const loopResult = await this.processTemplate(loopContent, loopContext, filters, depth + 1);
|
|
200
|
+
replacement += loopResult;
|
|
201
|
+
}
|
|
202
|
+
} else if (array) {
|
|
203
|
+
// Handle single item as array of one
|
|
204
|
+
const loopContext = {
|
|
205
|
+
...context,
|
|
206
|
+
[itemVar]: array,
|
|
207
|
+
loop: {
|
|
208
|
+
index: 0,
|
|
209
|
+
index0: 0,
|
|
210
|
+
index1: 1,
|
|
211
|
+
first: true,
|
|
212
|
+
last: true,
|
|
213
|
+
length: 1,
|
|
214
|
+
revindex: 1,
|
|
215
|
+
revindex0: 0
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
replacement = await this.processTemplate(loopContent, loopContext, filters, depth + 1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
processed = processed.substring(0, match.index) + replacement + processed.substring(match.index + fullMatch.length);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (this.options.strictMode) {
|
|
225
|
+
throw new Error(`Loop processing failed: ${error.message}`);
|
|
226
|
+
}
|
|
227
|
+
// Replace with empty string in non-strict mode
|
|
228
|
+
const replacement = '';
|
|
229
|
+
processed = processed.substring(0, match.index) + replacement + processed.substring(match.index + fullMatch.length);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return processed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Process include expressions
|
|
238
|
+
*/
|
|
239
|
+
async processIncludes(template, context, filters, depth) {
|
|
240
|
+
const includePattern = /\{\%\s*include\s+['"]([^'"]+)['"]\s*\%\}/g;
|
|
241
|
+
|
|
242
|
+
let match;
|
|
243
|
+
let processed = template;
|
|
244
|
+
|
|
245
|
+
// Process includes (simplified - no actual file loading for security)
|
|
246
|
+
while ((match = includePattern.exec(processed)) !== null) {
|
|
247
|
+
const [fullMatch, includePath] = match;
|
|
248
|
+
|
|
249
|
+
if (this.options.strictMode) {
|
|
250
|
+
throw new Error(`Include processing not implemented: ${includePath}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// In non-strict mode, replace with comment
|
|
254
|
+
const replacement = `<!-- Include: ${includePath} -->`;
|
|
255
|
+
processed = processed.replace(fullMatch, replacement);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return processed;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Process variable interpolations and filters
|
|
263
|
+
*/
|
|
264
|
+
processVariables(template, context, filters) {
|
|
265
|
+
return template.replace(this.patterns.variable, (match, expression) => {
|
|
266
|
+
try {
|
|
267
|
+
const trimmed = expression.trim();
|
|
268
|
+
|
|
269
|
+
// Check for filters: {{ variable | filter1 | filter2 }}
|
|
270
|
+
const parts = trimmed.split('|').map(p => p.trim());
|
|
271
|
+
let value = this.evaluateExpression(parts[0], context);
|
|
272
|
+
|
|
273
|
+
// Apply filters in sequence
|
|
274
|
+
for (let i = 1; i < parts.length; i++) {
|
|
275
|
+
const filterExpr = parts[i].trim();
|
|
276
|
+
|
|
277
|
+
// Parse filter with parentheses: filter(arg1, arg2) or filter arg1 arg2
|
|
278
|
+
let filterName, filterArgs = [];
|
|
279
|
+
|
|
280
|
+
if (filterExpr.includes('(')) {
|
|
281
|
+
// Handle filter(arg1, arg2) syntax
|
|
282
|
+
const match = filterExpr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\((.*)\)$/);
|
|
283
|
+
if (match) {
|
|
284
|
+
filterName = match[1];
|
|
285
|
+
const argsStr = match[2].trim();
|
|
286
|
+
if (argsStr) {
|
|
287
|
+
// Split arguments by comma, respecting quotes
|
|
288
|
+
filterArgs = this.parseFilterArguments(argsStr);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
throw new Error(`Invalid filter syntax: ${filterExpr}`);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
// Handle filter arg1 arg2 syntax
|
|
295
|
+
const parts = filterExpr.split(/\s+/);
|
|
296
|
+
filterName = parts[0];
|
|
297
|
+
filterArgs = parts.slice(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Parse filter arguments
|
|
301
|
+
const parsedArgs = filterArgs.map(arg => this.parseArgument(arg, context));
|
|
302
|
+
|
|
303
|
+
value = filters.apply(filterName, value, ...parsedArgs);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return String(value !== null && value !== undefined ? value : '');
|
|
307
|
+
} catch (error) {
|
|
308
|
+
if (this.options.strictMode) {
|
|
309
|
+
throw new Error(`Variable processing failed: ${error.message}`);
|
|
310
|
+
}
|
|
311
|
+
return match; // Return original expression on error
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Evaluate condition expression
|
|
318
|
+
*/
|
|
319
|
+
evaluateCondition(condition, context) {
|
|
320
|
+
// Simple condition evaluation
|
|
321
|
+
// Supports: variable, variable == value, variable != value, !variable
|
|
322
|
+
|
|
323
|
+
if (condition.includes('==')) {
|
|
324
|
+
const [left, right] = condition.split('==').map(s => s.trim());
|
|
325
|
+
return this.evaluateExpression(left, context) == this.parseValue(right, context);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (condition.includes('!=')) {
|
|
329
|
+
const [left, right] = condition.split('!=').map(s => s.trim());
|
|
330
|
+
return this.evaluateExpression(left, context) != this.parseValue(right, context);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (condition.startsWith('!')) {
|
|
334
|
+
const expr = condition.substring(1).trim();
|
|
335
|
+
return !this.isTruthy(this.evaluateExpression(expr, context));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Simple truthiness check
|
|
339
|
+
return this.isTruthy(this.evaluateExpression(condition, context));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Evaluate expression to get value from context
|
|
344
|
+
*/
|
|
345
|
+
evaluateExpression(expr, context) {
|
|
346
|
+
if (!expr) return '';
|
|
347
|
+
|
|
348
|
+
// Handle literals
|
|
349
|
+
if (expr.startsWith('"') && expr.endsWith('"')) {
|
|
350
|
+
return expr.slice(1, -1);
|
|
351
|
+
}
|
|
352
|
+
if (expr.startsWith("'") && expr.endsWith("'")) {
|
|
353
|
+
return expr.slice(1, -1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Handle numbers
|
|
357
|
+
if (/^-?\d+(\.\d+)?$/.test(expr)) {
|
|
358
|
+
return parseFloat(expr);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Handle booleans
|
|
362
|
+
if (expr === 'true') return true;
|
|
363
|
+
if (expr === 'false') return false;
|
|
364
|
+
if (expr === 'null') return null;
|
|
365
|
+
|
|
366
|
+
// Handle object property access
|
|
367
|
+
const parts = expr.split('.');
|
|
368
|
+
let value = context;
|
|
369
|
+
|
|
370
|
+
for (const part of parts) {
|
|
371
|
+
if (value === null || value === undefined) return '';
|
|
372
|
+
value = value[part];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return value !== undefined ? value : '';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Parse filter arguments from a comma-separated string, respecting quotes
|
|
380
|
+
*/
|
|
381
|
+
parseFilterArguments(argsStr) {
|
|
382
|
+
const args = [];
|
|
383
|
+
let current = '';
|
|
384
|
+
let inQuotes = false;
|
|
385
|
+
let quoteChar = null;
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
388
|
+
const char = argsStr[i];
|
|
389
|
+
|
|
390
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
391
|
+
inQuotes = true;
|
|
392
|
+
quoteChar = char;
|
|
393
|
+
current += char;
|
|
394
|
+
} else if (char === quoteChar && inQuotes) {
|
|
395
|
+
inQuotes = false;
|
|
396
|
+
quoteChar = null;
|
|
397
|
+
current += char;
|
|
398
|
+
} else if (char === ',' && !inQuotes) {
|
|
399
|
+
args.push(current.trim());
|
|
400
|
+
current = '';
|
|
401
|
+
} else {
|
|
402
|
+
current += char;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (current.trim()) {
|
|
407
|
+
args.push(current.trim());
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return args;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Parse argument value (string, number, or variable reference)
|
|
415
|
+
*/
|
|
416
|
+
parseArgument(arg, context) {
|
|
417
|
+
// Handle quoted strings
|
|
418
|
+
if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) {
|
|
419
|
+
return arg.slice(1, -1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Handle numbers
|
|
423
|
+
if (/^-?\d+(\.\d+)?$/.test(arg)) {
|
|
424
|
+
return parseFloat(arg);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Handle booleans
|
|
428
|
+
if (arg === 'true') return true;
|
|
429
|
+
if (arg === 'false') return false;
|
|
430
|
+
if (arg === 'null') return null;
|
|
431
|
+
|
|
432
|
+
// Handle variable reference
|
|
433
|
+
return this.evaluateExpression(arg, context);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Parse value with context substitution
|
|
438
|
+
*/
|
|
439
|
+
parseValue(value, context) {
|
|
440
|
+
return this.parseArgument(value, context);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Check if value is truthy in template context
|
|
445
|
+
*/
|
|
446
|
+
isTruthy(value) {
|
|
447
|
+
if (value === null || value === undefined) return false;
|
|
448
|
+
if (value === '') return false;
|
|
449
|
+
if (value === 0) return false;
|
|
450
|
+
if (value === false) return false;
|
|
451
|
+
if (Array.isArray(value) && value.length === 0) return false;
|
|
452
|
+
if (typeof value === 'object' && Object.keys(value).length === 0) return false;
|
|
453
|
+
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Get renderer statistics
|
|
459
|
+
*/
|
|
460
|
+
getStats() {
|
|
461
|
+
return {
|
|
462
|
+
...this.options,
|
|
463
|
+
maxDepthReached: this.renderDepth,
|
|
464
|
+
supportedExpressions: ['if/else/endif', 'for/endfor', 'include', 'variables', 'filters']
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export default KGenRenderer;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview CLI tool for documentation generation
|
|
4
|
+
* Usage: node cli.mjs [options]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseFiles } from './parser.mjs';
|
|
8
|
+
import { generateModulesMDX } from './mdx-generator.mjs';
|
|
9
|
+
import { scanWorkspace, groupFilesByModule } from './scanner.mjs';
|
|
10
|
+
import { generateNavigation, generatePackageIndex, generateAPIIndex } from './nav-generator.mjs';
|
|
11
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Main CLI function
|
|
20
|
+
*/
|
|
21
|
+
async function main() {
|
|
22
|
+
console.log('📚 @unrdf/kgn Documentation Generator\n');
|
|
23
|
+
|
|
24
|
+
// Default options
|
|
25
|
+
const rootDir = join(__dirname, '../../../..');
|
|
26
|
+
const outputDir = join(rootDir, 'packages/nextra/pages/api');
|
|
27
|
+
|
|
28
|
+
console.log(`Root: ${rootDir}`);
|
|
29
|
+
console.log(`Output: ${outputDir}\n`);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Phase 1: Scan workspace
|
|
33
|
+
console.log('🔍 Phase 1: Scanning workspace...');
|
|
34
|
+
const packages = await scanWorkspace(rootDir);
|
|
35
|
+
console.log(` Found ${packages.length} packages\n`);
|
|
36
|
+
|
|
37
|
+
if (packages.length === 0) {
|
|
38
|
+
console.log('⚠️ No packages found with source files');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Group files by module
|
|
43
|
+
const groupedFiles = groupFilesByModule(packages);
|
|
44
|
+
|
|
45
|
+
// Phase 2: Parse all files
|
|
46
|
+
console.log('📖 Phase 2: Parsing JSDoc...');
|
|
47
|
+
let totalFiles = 0;
|
|
48
|
+
const parsedByPackage = {};
|
|
49
|
+
|
|
50
|
+
for (const [pkgName, pkgData] of Object.entries(groupedFiles)) {
|
|
51
|
+
console.log(` Parsing @unrdf/${pkgName}...`);
|
|
52
|
+
const parsed = parseFiles(pkgData.sourceFiles, rootDir);
|
|
53
|
+
parsedByPackage[pkgName] = parsed;
|
|
54
|
+
totalFiles += parsed.length;
|
|
55
|
+
}
|
|
56
|
+
console.log(` Parsed ${totalFiles} files\n`);
|
|
57
|
+
|
|
58
|
+
// Phase 3: Generate MDX
|
|
59
|
+
console.log('📝 Phase 3: Generating MDX...');
|
|
60
|
+
let generatedCount = 0;
|
|
61
|
+
|
|
62
|
+
for (const [pkgName, parsed] of Object.entries(parsedByPackage)) {
|
|
63
|
+
const mdxMap = generateModulesMDX(parsed);
|
|
64
|
+
|
|
65
|
+
// Write MDX files
|
|
66
|
+
for (const [relativePath, mdx] of mdxMap) {
|
|
67
|
+
const fileName = relativePath.split('/').pop().replace(/\.(m?js)$/, '.mdx');
|
|
68
|
+
const pkgOutputDir = join(outputDir, pkgName);
|
|
69
|
+
|
|
70
|
+
// Create directory if needed
|
|
71
|
+
await mkdir(pkgOutputDir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
// Write MDX file
|
|
74
|
+
const mdxPath = join(pkgOutputDir, fileName);
|
|
75
|
+
await writeFile(mdxPath, mdx, 'utf-8');
|
|
76
|
+
|
|
77
|
+
generatedCount++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(` Generated ${mdxMap.size} files for @unrdf/${pkgName}`);
|
|
81
|
+
}
|
|
82
|
+
console.log(` Total: ${generatedCount} MDX files\n`);
|
|
83
|
+
|
|
84
|
+
// Phase 4: Generate navigation
|
|
85
|
+
console.log('🗂️ Phase 4: Generating navigation...');
|
|
86
|
+
|
|
87
|
+
// Generate _meta.json
|
|
88
|
+
const navigation = generateNavigation(groupedFiles);
|
|
89
|
+
const metaPath = join(outputDir, '_meta.json');
|
|
90
|
+
await writeFile(metaPath, JSON.stringify(navigation, null, 2), 'utf-8');
|
|
91
|
+
console.log(` Created _meta.json`);
|
|
92
|
+
|
|
93
|
+
// Generate index pages
|
|
94
|
+
const apiIndex = generateAPIIndex(groupedFiles);
|
|
95
|
+
const apiIndexPath = join(outputDir, 'index.mdx');
|
|
96
|
+
await writeFile(apiIndexPath, apiIndex, 'utf-8');
|
|
97
|
+
console.log(` Created API index`);
|
|
98
|
+
|
|
99
|
+
for (const [pkgName, pkgData] of Object.entries(groupedFiles)) {
|
|
100
|
+
const pkgIndex = generatePackageIndex(pkgName, pkgData);
|
|
101
|
+
const pkgIndexPath = join(outputDir, pkgName, 'index.mdx');
|
|
102
|
+
await writeFile(pkgIndexPath, pkgIndex, 'utf-8');
|
|
103
|
+
console.log(` Created @unrdf/${pkgName} index`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('\n✅ Documentation generation complete!');
|
|
107
|
+
console.log(`\n📊 Summary:`);
|
|
108
|
+
console.log(` Packages: ${packages.length}`);
|
|
109
|
+
console.log(` Source files: ${totalFiles}`);
|
|
110
|
+
console.log(` MDX files: ${generatedCount}`);
|
|
111
|
+
console.log(` Output: ${outputDir}`);
|
|
112
|
+
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('\n❌ Documentation generation failed:');
|
|
115
|
+
console.error(error.message);
|
|
116
|
+
console.error(error.stack);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Run CLI
|
|
122
|
+
main();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main orchestrator for JSDoc→RDF→MDX documentation generator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { parseFile, parseFiles } from './parser.mjs';
|
|
6
|
+
export { buildRDFGraph, buildRDFGraphs, buildJSONLD, PREFIXES } from './rdf-builder.mjs';
|
|
7
|
+
export { generateModuleMDX, generateModulesMDX } from './mdx-generator.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate documentation for a single file
|
|
11
|
+
* @param {string} filePath - Source file path
|
|
12
|
+
* @param {Object} options - Generation options
|
|
13
|
+
* @returns {Object} Generated documentation
|
|
14
|
+
*/
|
|
15
|
+
export async function generateDocs(filePath, options = {}) {
|
|
16
|
+
const { parseFile } = await import('./parser.mjs');
|
|
17
|
+
const { buildRDFGraph, buildJSONLD } = await import('./rdf-builder.mjs');
|
|
18
|
+
const { generateModuleMDX } = await import('./mdx-generator.mjs');
|
|
19
|
+
|
|
20
|
+
const parsed = parseFile(filePath, options.rootDir);
|
|
21
|
+
const rdf = buildRDFGraph(parsed);
|
|
22
|
+
const jsonld = buildJSONLD(parsed);
|
|
23
|
+
const mdx = generateModuleMDX(parsed);
|
|
24
|
+
|
|
25
|
+
return { parsed, rdf, jsonld, mdx };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default { generateDocs };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MDX Generator using @unrdf/kgn templates
|
|
3
|
+
* Converts parsed JSDoc + RDF data to MDX files for Nextra
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import nunjucks from 'nunjucks';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate MDX from parsed module data
|
|
16
|
+
* @param {Object} parsedModule - Output from parser.parseFile()
|
|
17
|
+
* @param {string} templatePath - Path to Nunjucks template
|
|
18
|
+
* @returns {string} Generated MDX content
|
|
19
|
+
*/
|
|
20
|
+
export function generateModuleMDX(parsedModule, templatePath = null) {
|
|
21
|
+
// Set up Nunjucks environment
|
|
22
|
+
const templatesDir = join(__dirname, '../../templates');
|
|
23
|
+
const env = new nunjucks.Environment(
|
|
24
|
+
new nunjucks.FileSystemLoader(templatesDir),
|
|
25
|
+
{ autoescape: false, trimBlocks: true, lstripBlocks: true }
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Add custom filters
|
|
29
|
+
env.addFilter('map', (arr, attr) => arr.map(item => item[attr]));
|
|
30
|
+
env.addFilter('join', (arr, sep) => arr.join(sep));
|
|
31
|
+
|
|
32
|
+
// Default template
|
|
33
|
+
if (!templatePath) {
|
|
34
|
+
templatePath = 'api/module.njk';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Prepare data for template
|
|
38
|
+
const templateData = {
|
|
39
|
+
module: {
|
|
40
|
+
name: parsedModule.relativePath.split('/').pop().replace(/\.(m?js|ts)$/, ''),
|
|
41
|
+
relativePath: parsedModule.relativePath,
|
|
42
|
+
file: parsedModule.file,
|
|
43
|
+
exports: parsedModule.exports,
|
|
44
|
+
imports: parsedModule.imports,
|
|
45
|
+
commentCount: parsedModule.comments,
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Render template
|
|
50
|
+
return env.render(templatePath, templateData);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate MDX for multiple modules
|
|
55
|
+
* @param {Array} parsedModules - Array of parsed module data
|
|
56
|
+
* @returns {Map} Map of file paths to MDX content
|
|
57
|
+
*/
|
|
58
|
+
export function generateModulesMDX(parsedModules) {
|
|
59
|
+
const mdxMap = new Map();
|
|
60
|
+
|
|
61
|
+
parsedModules.forEach(module => {
|
|
62
|
+
if (module.error) return;
|
|
63
|
+
|
|
64
|
+
const mdx = generateModuleMDX(module);
|
|
65
|
+
mdxMap.set(module.relativePath, mdx);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return mdxMap;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default { generateModuleMDX, generateModulesMDX };
|