@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,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGEN Injection Integration
|
|
3
|
+
*
|
|
4
|
+
* Integrates injection capabilities with the existing KGEN template engine.
|
|
5
|
+
* Provides seamless injection support for template processing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { inject, dryRun, processTemplate, initializeInjection } from './api.js';
|
|
9
|
+
import { DEFAULT_CONFIG, INJECTION_MODES } from './constants.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Enhance KGEN engine with injection capabilities
|
|
13
|
+
*/
|
|
14
|
+
export function enhanceKgenWithInjection(kgenEngine, injectionConfig = {}) {
|
|
15
|
+
const config = { ...DEFAULT_CONFIG, ...injectionConfig };
|
|
16
|
+
|
|
17
|
+
// Initialize injection system
|
|
18
|
+
const injectionEngine = initializeInjection(config);
|
|
19
|
+
|
|
20
|
+
// Store original methods
|
|
21
|
+
const originalRender = kgenEngine.render;
|
|
22
|
+
const originalRenderString = kgenEngine.renderString;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Enhanced render method with injection support
|
|
26
|
+
*/
|
|
27
|
+
kgenEngine.render = async function(templatePath, data = {}, options = {}) {
|
|
28
|
+
// First, try to read and parse the template
|
|
29
|
+
const template = await this.getTemplate(templatePath);
|
|
30
|
+
|
|
31
|
+
// Check if template has injection configuration in frontmatter
|
|
32
|
+
if (template.frontmatter && template.frontmatter.inject) {
|
|
33
|
+
return await processInjectionTemplate(template, data, options, injectionEngine);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fall back to original render for non-injection templates
|
|
37
|
+
return originalRender.call(this, templatePath, data, options);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Enhanced renderString with injection support
|
|
42
|
+
*/
|
|
43
|
+
kgenEngine.renderString = async function(templateString, data = {}, options = {}) {
|
|
44
|
+
// Parse template string for frontmatter
|
|
45
|
+
const parsed = parseTemplateString(templateString);
|
|
46
|
+
|
|
47
|
+
if (parsed.frontmatter && parsed.frontmatter.inject) {
|
|
48
|
+
return await processInjectionTemplate(parsed, data, options, injectionEngine);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fall back to original renderString
|
|
52
|
+
return originalRenderString.call(this, templateString, data, options);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add injection-specific methods to the engine
|
|
57
|
+
*/
|
|
58
|
+
kgenEngine.inject = async function(templateConfig, content, variables = {}) {
|
|
59
|
+
return await inject(templateConfig, content, variables, { config });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
kgenEngine.dryRunInjection = async function(templateConfig, content, variables = {}) {
|
|
63
|
+
return await dryRun(templateConfig, content, variables, { config });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
kgenEngine.getInjectionHistory = function() {
|
|
67
|
+
return injectionEngine.getOperationHistory();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
kgenEngine.undoInjection = async function(operationId) {
|
|
71
|
+
return await injectionEngine.undo(operationId);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Batch template processing with injection support
|
|
76
|
+
*/
|
|
77
|
+
kgenEngine.processBatch = async function(templates, globalData = {}, options = {}) {
|
|
78
|
+
const results = [];
|
|
79
|
+
const errors = [];
|
|
80
|
+
|
|
81
|
+
for (const template of templates) {
|
|
82
|
+
try {
|
|
83
|
+
const templateData = { ...globalData, ...(template.data || {}) };
|
|
84
|
+
let result;
|
|
85
|
+
|
|
86
|
+
if (template.content) {
|
|
87
|
+
result = await this.renderString(template.content, templateData, options);
|
|
88
|
+
} else if (template.path) {
|
|
89
|
+
result = await this.render(template.path, templateData, options);
|
|
90
|
+
} else {
|
|
91
|
+
throw new Error('Template must have either content or path');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
results.push({
|
|
95
|
+
template,
|
|
96
|
+
result,
|
|
97
|
+
success: true
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
} catch (error) {
|
|
101
|
+
errors.push({
|
|
102
|
+
template,
|
|
103
|
+
error: error.message,
|
|
104
|
+
success: false
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!options.continueOnError) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
results,
|
|
115
|
+
errors,
|
|
116
|
+
total: templates.length,
|
|
117
|
+
successful: results.length,
|
|
118
|
+
failed: errors.length
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return kgenEngine;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Process injection template
|
|
127
|
+
*/
|
|
128
|
+
async function processInjectionTemplate(template, data, options, injectionEngine) {
|
|
129
|
+
const { frontmatter, content } = template;
|
|
130
|
+
|
|
131
|
+
// Render the template content first
|
|
132
|
+
const renderedContent = await renderTemplateContent(content, data, options);
|
|
133
|
+
|
|
134
|
+
// Perform injection
|
|
135
|
+
const injectionResult = await injectionEngine.inject(frontmatter, renderedContent, data);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
injectionResult,
|
|
139
|
+
operationId: injectionResult.operationId,
|
|
140
|
+
success: injectionResult.success,
|
|
141
|
+
skipped: injectionResult.skipped,
|
|
142
|
+
targets: injectionResult.targets,
|
|
143
|
+
content: renderedContent
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Render template content (without injection)
|
|
149
|
+
*/
|
|
150
|
+
async function renderTemplateContent(content, data, options) {
|
|
151
|
+
// Simple variable interpolation - in production use proper template engine
|
|
152
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
|
|
153
|
+
return data[variable] || match;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse template string for frontmatter
|
|
159
|
+
*/
|
|
160
|
+
function parseTemplateString(templateString) {
|
|
161
|
+
const frontmatterMatch = templateString.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
162
|
+
|
|
163
|
+
if (!frontmatterMatch) {
|
|
164
|
+
return {
|
|
165
|
+
frontmatter: {},
|
|
166
|
+
content: templateString
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const frontmatter = parseFrontmatter(frontmatterMatch[1]);
|
|
171
|
+
const content = frontmatterMatch[2];
|
|
172
|
+
|
|
173
|
+
return { frontmatter, content };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Simple frontmatter parser
|
|
178
|
+
*/
|
|
179
|
+
function parseFrontmatter(text) {
|
|
180
|
+
const result = {};
|
|
181
|
+
const lines = text.split('\n');
|
|
182
|
+
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
const trimmed = line.trim();
|
|
185
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
186
|
+
|
|
187
|
+
const colonIndex = trimmed.indexOf(':');
|
|
188
|
+
if (colonIndex === -1) continue;
|
|
189
|
+
|
|
190
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
191
|
+
let value = trimmed.substring(colonIndex + 1).trim();
|
|
192
|
+
|
|
193
|
+
// Parse arrays
|
|
194
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
195
|
+
value = value.slice(1, -1).split(',').map(v => v.trim().replace(/['"]/g, ''));
|
|
196
|
+
}
|
|
197
|
+
// Parse booleans
|
|
198
|
+
else if (value === 'true') {
|
|
199
|
+
value = true;
|
|
200
|
+
} else if (value === 'false') {
|
|
201
|
+
value = false;
|
|
202
|
+
}
|
|
203
|
+
// Parse numbers
|
|
204
|
+
else if (/^\d+$/.test(value)) {
|
|
205
|
+
value = parseInt(value);
|
|
206
|
+
}
|
|
207
|
+
// Remove quotes
|
|
208
|
+
else if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
209
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
210
|
+
value = value.slice(1, -1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
result[key] = value;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create injection-aware template loader
|
|
221
|
+
*/
|
|
222
|
+
export function createInjectionTemplateLoader(baseLoader, injectionConfig = {}) {
|
|
223
|
+
return {
|
|
224
|
+
...baseLoader,
|
|
225
|
+
|
|
226
|
+
async loadTemplate(templatePath, options = {}) {
|
|
227
|
+
const template = await baseLoader.loadTemplate(templatePath, options);
|
|
228
|
+
|
|
229
|
+
// Parse for injection configuration
|
|
230
|
+
const parsed = parseTemplateString(template.content || template);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
...template,
|
|
234
|
+
frontmatter: parsed.frontmatter,
|
|
235
|
+
content: parsed.content,
|
|
236
|
+
hasInjection: !!(parsed.frontmatter && parsed.frontmatter.inject),
|
|
237
|
+
injectionMode: parsed.frontmatter?.mode || INJECTION_MODES.APPEND
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Validation helpers for injection templates
|
|
245
|
+
*/
|
|
246
|
+
export const injectionValidators = {
|
|
247
|
+
/**
|
|
248
|
+
* Validate injection template configuration
|
|
249
|
+
*/
|
|
250
|
+
validateTemplate(template) {
|
|
251
|
+
const errors = [];
|
|
252
|
+
|
|
253
|
+
if (!template.frontmatter) {
|
|
254
|
+
return { valid: true, errors: [] }; // Non-injection template
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const fm = template.frontmatter;
|
|
258
|
+
|
|
259
|
+
if (fm.inject && !fm.to) {
|
|
260
|
+
errors.push('Injection templates must specify "to" target');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (fm.mode && !Object.values(INJECTION_MODES).includes(fm.mode)) {
|
|
264
|
+
errors.push(`Invalid injection mode: ${fm.mode}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (fm.mode === INJECTION_MODES.BEFORE && !fm.target) {
|
|
268
|
+
errors.push('Before mode requires "target" pattern');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (fm.mode === INJECTION_MODES.AFTER && !fm.target) {
|
|
272
|
+
errors.push('After mode requires "target" pattern');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (fm.mode === INJECTION_MODES.REPLACE && !fm.target) {
|
|
276
|
+
errors.push('Replace mode requires "target" pattern');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (fm.mode === INJECTION_MODES.LINE_AT && typeof fm.lineNumber !== 'number') {
|
|
280
|
+
errors.push('LineAt mode requires "lineNumber" as number');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
valid: errors.length === 0,
|
|
285
|
+
errors
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Validate injection context data
|
|
291
|
+
*/
|
|
292
|
+
validateData(data, template) {
|
|
293
|
+
const errors = [];
|
|
294
|
+
const fm = template.frontmatter;
|
|
295
|
+
|
|
296
|
+
if (!fm || !fm.inject) {
|
|
297
|
+
return { valid: true, errors: [] };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check for required variables in template content
|
|
301
|
+
const requiredVars = extractTemplateVariables(template.content);
|
|
302
|
+
|
|
303
|
+
for (const variable of requiredVars) {
|
|
304
|
+
if (!(variable in data)) {
|
|
305
|
+
errors.push(`Required template variable missing: ${variable}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
valid: errors.length === 0,
|
|
311
|
+
errors
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Extract template variables from content
|
|
318
|
+
*/
|
|
319
|
+
function extractTemplateVariables(content) {
|
|
320
|
+
const variables = new Set();
|
|
321
|
+
const matches = content.matchAll(/\{\{(\w+)\}\}/g);
|
|
322
|
+
|
|
323
|
+
for (const match of matches) {
|
|
324
|
+
variables.add(match[1]);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return Array.from(variables);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Export integration utilities
|
|
332
|
+
*/
|
|
333
|
+
export {
|
|
334
|
+
inject,
|
|
335
|
+
dryRun,
|
|
336
|
+
initializeInjection,
|
|
337
|
+
INJECTION_MODES,
|
|
338
|
+
DEFAULT_CONFIG
|
|
339
|
+
};
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGEN Injection Modes
|
|
3
|
+
*
|
|
4
|
+
* Implements different injection modes for deterministic content modification.
|
|
5
|
+
* Each mode provides atomic and idempotent content insertion strategies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { INJECTION_MODES, LINE_ENDINGS } from '../constants.js';
|
|
9
|
+
|
|
10
|
+
export class InjectionModes {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Apply injection mode to content
|
|
17
|
+
*/
|
|
18
|
+
async applyMode(mode, currentContent, injectionContent, target, variables = {}) {
|
|
19
|
+
const normalizedCurrent = this._normalizeContent(currentContent);
|
|
20
|
+
const normalizedInjection = this._normalizeInjectionContent(injectionContent, variables);
|
|
21
|
+
|
|
22
|
+
switch (mode) {
|
|
23
|
+
case INJECTION_MODES.APPEND:
|
|
24
|
+
return this._applyAppend(normalizedCurrent, normalizedInjection, target);
|
|
25
|
+
|
|
26
|
+
case INJECTION_MODES.PREPEND:
|
|
27
|
+
return this._applyPrepend(normalizedCurrent, normalizedInjection, target);
|
|
28
|
+
|
|
29
|
+
case INJECTION_MODES.BEFORE:
|
|
30
|
+
return this._applyBefore(normalizedCurrent, normalizedInjection, target);
|
|
31
|
+
|
|
32
|
+
case INJECTION_MODES.AFTER:
|
|
33
|
+
return this._applyAfter(normalizedCurrent, normalizedInjection, target);
|
|
34
|
+
|
|
35
|
+
case INJECTION_MODES.REPLACE:
|
|
36
|
+
return this._applyReplace(normalizedCurrent, normalizedInjection, target);
|
|
37
|
+
|
|
38
|
+
case INJECTION_MODES.LINE_AT:
|
|
39
|
+
return this._applyLineAt(normalizedCurrent, normalizedInjection, target);
|
|
40
|
+
|
|
41
|
+
case INJECTION_MODES.CREATE:
|
|
42
|
+
return this._applyCreate(normalizedCurrent, normalizedInjection, target);
|
|
43
|
+
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(`Unknown injection mode: ${mode}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Append content to end of file
|
|
51
|
+
*/
|
|
52
|
+
_applyAppend(currentContent, injectionContent, target) {
|
|
53
|
+
const lineEnding = this._detectLineEnding(currentContent);
|
|
54
|
+
const ensureNewline = target.ensureNewline !== false;
|
|
55
|
+
|
|
56
|
+
let result = currentContent;
|
|
57
|
+
|
|
58
|
+
// Add newline before injection if file doesn't end with one
|
|
59
|
+
if (ensureNewline && currentContent && !currentContent.endsWith(lineEnding)) {
|
|
60
|
+
result += lineEnding;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
result += injectionContent;
|
|
64
|
+
|
|
65
|
+
// Ensure file ends with newline if configured
|
|
66
|
+
if (target.ensureTrailingNewline && !result.endsWith(lineEnding)) {
|
|
67
|
+
result += lineEnding;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Prepend content to beginning of file
|
|
75
|
+
*/
|
|
76
|
+
_applyPrepend(currentContent, injectionContent, target) {
|
|
77
|
+
const lineEnding = this._detectLineEnding(currentContent);
|
|
78
|
+
let result = injectionContent;
|
|
79
|
+
|
|
80
|
+
// Add newline after injection if it doesn't end with one
|
|
81
|
+
if (!injectionContent.endsWith(lineEnding)) {
|
|
82
|
+
result += lineEnding;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
result += currentContent;
|
|
86
|
+
|
|
87
|
+
// Sort imports if enabled and content looks like imports
|
|
88
|
+
if (target.sortImports && this._looksLikeImports(injectionContent)) {
|
|
89
|
+
result = this._sortImports(result);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Insert content before specific target
|
|
97
|
+
*/
|
|
98
|
+
_applyBefore(currentContent, injectionContent, target) {
|
|
99
|
+
const targetPattern = target.target;
|
|
100
|
+
if (!targetPattern) {
|
|
101
|
+
throw new Error('Before mode requires target pattern');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const lineEnding = this._detectLineEnding(currentContent);
|
|
105
|
+
const lines = currentContent.split(lineEnding);
|
|
106
|
+
const insertionLines = injectionContent.split(lineEnding);
|
|
107
|
+
|
|
108
|
+
// Find target line
|
|
109
|
+
const targetIndex = this._findTargetLine(lines, targetPattern, target);
|
|
110
|
+
if (targetIndex === -1) {
|
|
111
|
+
throw new Error(`Target pattern not found: ${targetPattern}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Preserve indentation if configured
|
|
115
|
+
if (target.preserveIndentation) {
|
|
116
|
+
const targetIndentation = this._getLineIndentation(lines[targetIndex]);
|
|
117
|
+
for (let i = 0; i < insertionLines.length; i++) {
|
|
118
|
+
if (insertionLines[i].trim()) {
|
|
119
|
+
insertionLines[i] = targetIndentation + insertionLines[i].trim();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Insert before target line
|
|
125
|
+
lines.splice(targetIndex, 0, ...insertionLines);
|
|
126
|
+
|
|
127
|
+
return lines.join(lineEnding);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Insert content after specific target
|
|
132
|
+
*/
|
|
133
|
+
_applyAfter(currentContent, injectionContent, target) {
|
|
134
|
+
const targetPattern = target.target;
|
|
135
|
+
if (!targetPattern) {
|
|
136
|
+
throw new Error('After mode requires target pattern');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const lineEnding = this._detectLineEnding(currentContent);
|
|
140
|
+
const lines = currentContent.split(lineEnding);
|
|
141
|
+
const insertionLines = injectionContent.split(lineEnding);
|
|
142
|
+
|
|
143
|
+
// Find target line
|
|
144
|
+
const targetIndex = this._findTargetLine(lines, targetPattern, target);
|
|
145
|
+
if (targetIndex === -1) {
|
|
146
|
+
throw new Error(`Target pattern not found: ${targetPattern}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Preserve indentation if configured
|
|
150
|
+
if (target.preserveIndentation) {
|
|
151
|
+
const targetIndentation = this._getLineIndentation(lines[targetIndex]);
|
|
152
|
+
for (let i = 0; i < insertionLines.length; i++) {
|
|
153
|
+
if (insertionLines[i].trim()) {
|
|
154
|
+
insertionLines[i] = targetIndentation + insertionLines[i].trim();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Insert after target line
|
|
160
|
+
lines.splice(targetIndex + 1, 0, ...insertionLines);
|
|
161
|
+
|
|
162
|
+
return lines.join(lineEnding);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Replace specific content
|
|
167
|
+
*/
|
|
168
|
+
_applyReplace(currentContent, injectionContent, target) {
|
|
169
|
+
const targetPattern = target.target;
|
|
170
|
+
if (!targetPattern) {
|
|
171
|
+
throw new Error('Replace mode requires target pattern');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let result = currentContent;
|
|
175
|
+
|
|
176
|
+
if (target.regex) {
|
|
177
|
+
// Regex replacement
|
|
178
|
+
const flags = target.regexFlags || 'g';
|
|
179
|
+
const regex = new RegExp(targetPattern, flags);
|
|
180
|
+
result = result.replace(regex, injectionContent);
|
|
181
|
+
} else if (target.exact) {
|
|
182
|
+
// Exact string replacement
|
|
183
|
+
result = result.replace(targetPattern, injectionContent);
|
|
184
|
+
} else {
|
|
185
|
+
// First occurrence replacement
|
|
186
|
+
const index = result.indexOf(targetPattern);
|
|
187
|
+
if (index === -1) {
|
|
188
|
+
throw new Error(`Target pattern not found: ${targetPattern}`);
|
|
189
|
+
}
|
|
190
|
+
result = result.substring(0, index) + injectionContent + result.substring(index + targetPattern.length);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Insert content at specific line number
|
|
198
|
+
*/
|
|
199
|
+
_applyLineAt(currentContent, injectionContent, target) {
|
|
200
|
+
const lineNumber = target.lineNumber;
|
|
201
|
+
if (typeof lineNumber !== 'number') {
|
|
202
|
+
throw new Error('LineAt mode requires lineNumber');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const lineEnding = this._detectLineEnding(currentContent);
|
|
206
|
+
const lines = currentContent.split(lineEnding);
|
|
207
|
+
const insertionLines = injectionContent.split(lineEnding);
|
|
208
|
+
|
|
209
|
+
// Validate line number
|
|
210
|
+
if (target.validateTarget && (lineNumber < 1 || lineNumber > lines.length + 1)) {
|
|
211
|
+
throw new Error(`Invalid line number: ${lineNumber}. File has ${lines.length} lines.`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Convert to 0-based index
|
|
215
|
+
const insertIndex = lineNumber - 1;
|
|
216
|
+
|
|
217
|
+
// Insert at specified line
|
|
218
|
+
lines.splice(insertIndex, 0, ...insertionLines);
|
|
219
|
+
|
|
220
|
+
return lines.join(lineEnding);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create new file content
|
|
225
|
+
*/
|
|
226
|
+
_applyCreate(currentContent, injectionContent, target) {
|
|
227
|
+
// If file exists and not empty, handle based on configuration
|
|
228
|
+
if (currentContent && target.failIfExists) {
|
|
229
|
+
throw new Error('File already exists and failIfExists is true');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (currentContent && target.appendIfExists) {
|
|
233
|
+
return this._applyAppend(currentContent, injectionContent, target);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Return injection content as new file content
|
|
237
|
+
return injectionContent;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Helper Methods
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
_normalizeContent(content) {
|
|
245
|
+
if (!content) return '';
|
|
246
|
+
|
|
247
|
+
// Normalize line endings if configured
|
|
248
|
+
if (this.config.consistentLineEndings) {
|
|
249
|
+
const targetEnding = this.config.lineEnding || LINE_ENDINGS.LF;
|
|
250
|
+
return content.replace(/\r\n|\r|\n/g, targetEnding);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return content;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_normalizeInjectionContent(content, variables) {
|
|
257
|
+
// Variable interpolation
|
|
258
|
+
let result = content.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
|
|
259
|
+
return variables[variable] || match;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Trim if configured
|
|
263
|
+
if (this.config.trimInjectedContent) {
|
|
264
|
+
result = result.trim();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_detectLineEnding(content) {
|
|
271
|
+
if (content.includes('\r\n')) return LINE_ENDINGS.CRLF;
|
|
272
|
+
if (content.includes('\r')) return LINE_ENDINGS.CR;
|
|
273
|
+
return LINE_ENDINGS.LF;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_findTargetLine(lines, pattern, target) {
|
|
277
|
+
if (target.regex) {
|
|
278
|
+
const flags = target.regexFlags || 'gm';
|
|
279
|
+
const regex = new RegExp(pattern, flags);
|
|
280
|
+
return lines.findIndex(line => regex.test(line));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// String search (first occurrence for deterministic behavior)
|
|
284
|
+
return lines.findIndex(line => line.includes(pattern));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_getLineIndentation(line) {
|
|
288
|
+
const match = line.match(/^(\s*)/);
|
|
289
|
+
return match ? match[1] : '';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_looksLikeImports(content) {
|
|
293
|
+
return /^import\s+.*from\s+['"][^'"]+['"];?\s*$/m.test(content);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_sortImports(content) {
|
|
297
|
+
const lineEnding = this._detectLineEnding(content);
|
|
298
|
+
const lines = content.split(lineEnding);
|
|
299
|
+
|
|
300
|
+
// Find import blocks
|
|
301
|
+
const importBlocks = [];
|
|
302
|
+
let currentBlock = [];
|
|
303
|
+
let inImportBlock = false;
|
|
304
|
+
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
if (/^import\s+/.test(line.trim())) {
|
|
307
|
+
inImportBlock = true;
|
|
308
|
+
currentBlock.push(line);
|
|
309
|
+
} else if (inImportBlock && line.trim() === '') {
|
|
310
|
+
currentBlock.push(line);
|
|
311
|
+
} else if (inImportBlock) {
|
|
312
|
+
importBlocks.push([...currentBlock]);
|
|
313
|
+
currentBlock = [];
|
|
314
|
+
inImportBlock = false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Sort imports within each block deterministically
|
|
319
|
+
for (const block of importBlocks) {
|
|
320
|
+
block.sort((a, b) => {
|
|
321
|
+
const aImport = a.trim();
|
|
322
|
+
const bImport = b.trim();
|
|
323
|
+
|
|
324
|
+
// Sort by import source (deterministic)
|
|
325
|
+
const aSource = this._extractImportSource(aImport);
|
|
326
|
+
const bSource = this._extractImportSource(bImport);
|
|
327
|
+
|
|
328
|
+
return aSource.localeCompare(bSource);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Reconstruct content with sorted imports
|
|
333
|
+
// This is a simplified implementation - real sorting would be more complex
|
|
334
|
+
return content;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
_extractImportSource(importLine) {
|
|
338
|
+
const match = importLine.match(/from\s+['"]([^'"]+)['"]/);
|
|
339
|
+
return match ? match[1] : '';
|
|
340
|
+
}
|
|
341
|
+
}
|