@gregorlohaus/tdir 0.1.3 → 0.1.4
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/README.md +19 -2
- package/dist/cli.js +86 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +242 -63
- package/dist/render.d.ts +14 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,7 +142,7 @@ Pass a string to choose a custom JSON path inside the output directory:
|
|
|
142
142
|
render("./output", context, { reverseMap: "meta/reverse-map.json" })
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
The map contains a flat lookup from rendered strings to template tokens
|
|
145
|
+
The map contains a flat lookup from rendered strings to template tokens, per-file occurrences with path/range context, inline conditional blocks, and template files skipped by path conditionals:
|
|
146
146
|
|
|
147
147
|
```json
|
|
148
148
|
{
|
|
@@ -160,10 +160,27 @@ The map contains a flat lookup from rendered strings to template tokens plus per
|
|
|
160
160
|
"outputPath": "web/index.html",
|
|
161
161
|
"templatePath": "<@if(context.web)>web/index.html",
|
|
162
162
|
"range": { "start": 16, "end": 21 }
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"kind": "conditional",
|
|
166
|
+
"result": "<head><@var(context.header.title)></head>",
|
|
167
|
+
"token": "<@if(context.header.show)><head><@var(context.header.title)></head><@endif>",
|
|
168
|
+
"outputPath": "web/index.html",
|
|
169
|
+
"templatePath": "<@if(context.web)>web/index.html",
|
|
170
|
+
"range": { "start": 9, "end": 53 },
|
|
171
|
+
"activeRange": { "start": 27, "end": 71 }
|
|
163
172
|
}
|
|
164
173
|
]
|
|
165
174
|
}
|
|
166
175
|
],
|
|
176
|
+
"skipped": [
|
|
177
|
+
{
|
|
178
|
+
"kind": "file",
|
|
179
|
+
"templatePath": "<@if(context.docs)>docs/readme.md",
|
|
180
|
+
"encoding": "utf8",
|
|
181
|
+
"content": "# Docs"
|
|
182
|
+
}
|
|
183
|
+
],
|
|
167
184
|
"tokens": {
|
|
168
185
|
"Hello": ["<@var(context.header.title)>"]
|
|
169
186
|
}
|
|
@@ -191,7 +208,7 @@ tdir reverse ./output ./templates --map meta/reverse-map.json
|
|
|
191
208
|
bunx @gregorlohaus/tdir reverse ./output ./templates --map meta/reverse-map.json
|
|
192
209
|
```
|
|
193
210
|
|
|
194
|
-
The command writes files at their original template paths
|
|
211
|
+
The command writes files at their original template paths, restores recorded `<@var(...)>` tokens, wraps edited inline conditional output back in the original conditional block, and restores template files that were skipped by path conditionals.
|
|
195
212
|
|
|
196
213
|
## Unmatched directives
|
|
197
214
|
|
package/dist/cli.js
CHANGED
|
@@ -55,10 +55,66 @@ function replaceFirst(content, token) {
|
|
|
55
55
|
return null;
|
|
56
56
|
return `${content.slice(0, index)}${token.token}${content.slice(index + token.result.length)}`;
|
|
57
57
|
}
|
|
58
|
+
function applyActiveBranch(token, branchContent) {
|
|
59
|
+
if (!token.activeRange)
|
|
60
|
+
return token.token;
|
|
61
|
+
return [
|
|
62
|
+
token.token.slice(0, token.activeRange.start),
|
|
63
|
+
branchContent,
|
|
64
|
+
token.token.slice(token.activeRange.end)
|
|
65
|
+
].join("");
|
|
66
|
+
}
|
|
67
|
+
function findPrefixEnd(content, before) {
|
|
68
|
+
if (before === "")
|
|
69
|
+
return 0;
|
|
70
|
+
let candidate = before;
|
|
71
|
+
while (candidate.length >= 8) {
|
|
72
|
+
const index = content.indexOf(candidate);
|
|
73
|
+
if (index !== -1)
|
|
74
|
+
return index + candidate.length;
|
|
75
|
+
candidate = candidate.slice(-Math.max(1, Math.floor(candidate.length / 2)));
|
|
76
|
+
}
|
|
77
|
+
return -1;
|
|
78
|
+
}
|
|
79
|
+
function findSuffixStart(content, after, from) {
|
|
80
|
+
if (after === "")
|
|
81
|
+
return content.length;
|
|
82
|
+
let candidate = after;
|
|
83
|
+
while (candidate.length >= 8) {
|
|
84
|
+
const index = content.indexOf(candidate, from);
|
|
85
|
+
if (index !== -1)
|
|
86
|
+
return index;
|
|
87
|
+
candidate = candidate.slice(0, Math.floor(candidate.length / 2));
|
|
88
|
+
}
|
|
89
|
+
return -1;
|
|
90
|
+
}
|
|
91
|
+
function replaceConditional(content, token) {
|
|
92
|
+
const exactIndex = content.indexOf(token.result);
|
|
93
|
+
if (exactIndex !== -1) {
|
|
94
|
+
return [
|
|
95
|
+
content.slice(0, exactIndex),
|
|
96
|
+
token.token,
|
|
97
|
+
content.slice(exactIndex + token.result.length)
|
|
98
|
+
].join("");
|
|
99
|
+
}
|
|
100
|
+
if (token.before === undefined || token.after === undefined)
|
|
101
|
+
return null;
|
|
102
|
+
const branchStart = findPrefixEnd(content, token.before);
|
|
103
|
+
if (branchStart === -1)
|
|
104
|
+
return null;
|
|
105
|
+
const afterIndex = findSuffixStart(content, token.after, branchStart);
|
|
106
|
+
if (afterIndex === -1)
|
|
107
|
+
return null;
|
|
108
|
+
return [
|
|
109
|
+
content.slice(0, branchStart),
|
|
110
|
+
applyActiveBranch(token, content.slice(branchStart, afterIndex)),
|
|
111
|
+
content.slice(afterIndex)
|
|
112
|
+
].join("");
|
|
113
|
+
}
|
|
58
114
|
function reverseContent(content, file, warnings) {
|
|
59
|
-
const
|
|
115
|
+
const contentTokens = file.tokens.filter((token) => token.kind === "content").sort((a, b) => (b.range?.start ?? -1) - (a.range?.start ?? -1));
|
|
60
116
|
let reversed = content;
|
|
61
|
-
for (const token of
|
|
117
|
+
for (const token of contentTokens) {
|
|
62
118
|
const rangeResult = replaceAtRange(reversed, token);
|
|
63
119
|
if (rangeResult !== null) {
|
|
64
120
|
reversed = rangeResult;
|
|
@@ -76,8 +132,33 @@ function reverseContent(content, file, warnings) {
|
|
|
76
132
|
message: "Rendered value was not found; token was not restored"
|
|
77
133
|
});
|
|
78
134
|
}
|
|
135
|
+
const conditionalTokens = file.tokens.filter((token) => token.kind === "conditional").sort((a, b) => (b.range?.start ?? -1) - (a.range?.start ?? -1));
|
|
136
|
+
for (const token of conditionalTokens) {
|
|
137
|
+
const result = replaceConditional(reversed, token);
|
|
138
|
+
if (result !== null) {
|
|
139
|
+
reversed = result;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
warnings.push({
|
|
143
|
+
outputPath: file.outputPath,
|
|
144
|
+
token: token.token,
|
|
145
|
+
result: token.result,
|
|
146
|
+
message: "Rendered conditional block was not found; block was not restored"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
79
149
|
return reversed;
|
|
80
150
|
}
|
|
151
|
+
function writeSkippedTemplate(templateRoot, skipped) {
|
|
152
|
+
const templatePath = resolveInside(templateRoot, skipped.templatePath);
|
|
153
|
+
if (skipped.kind === "directory") {
|
|
154
|
+
mkdirSync(templatePath, { recursive: true });
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
mkdirSync(dirname(templatePath), { recursive: true });
|
|
158
|
+
const content = skipped.encoding === "base64" ? Buffer.from(skipped.content ?? "", "base64") : skipped.content ?? "";
|
|
159
|
+
writeFileSync(templatePath, content);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
81
162
|
function reverseDir(renderedDir, templateDir, options = {}) {
|
|
82
163
|
const renderedRoot = resolvePath(renderedDir);
|
|
83
164
|
const templateRoot = resolvePath(templateDir);
|
|
@@ -114,6 +195,9 @@ function reverseDir(renderedDir, templateDir, options = {}) {
|
|
|
114
195
|
writeFileSync(templatePath, reverseContent(content.toString("utf-8"), file, warnings));
|
|
115
196
|
filesWritten += 1;
|
|
116
197
|
}
|
|
198
|
+
for (const skipped of manifest.skipped ?? []) {
|
|
199
|
+
filesWritten += writeSkippedTemplate(templateRoot, skipped);
|
|
200
|
+
}
|
|
117
201
|
return { filesWritten, warnings };
|
|
118
202
|
}
|
|
119
203
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type RenderOptions } from "./render";
|
|
3
|
-
export type { RenderOptions, ReverseMapFile, ReverseMapManifest, ReverseMapToken } from "./render";
|
|
3
|
+
export type { RenderOptions, ReverseMapFile, ReverseMapManifest, ReverseMapStoredTemplate, ReverseMapToken } from "./render";
|
|
4
4
|
export { reverseDir, type ReverseOptions, type ReverseResult, type ReverseWarning } from "./reverse";
|
|
5
5
|
interface Stringable {
|
|
6
6
|
toString: () => string;
|
package/dist/index.js
CHANGED
|
@@ -175,7 +175,7 @@ function resolveOutputPath(destRoot, outputName) {
|
|
|
175
175
|
return outputPath;
|
|
176
176
|
}
|
|
177
177
|
function createReverseMapManifest() {
|
|
178
|
-
return { version: 1, files: [], tokens: {} };
|
|
178
|
+
return { version: 1, files: [], skipped: [], tokens: {} };
|
|
179
179
|
}
|
|
180
180
|
function addReverseMapToken(state, file, token) {
|
|
181
181
|
if (!state?.manifest || !file)
|
|
@@ -231,69 +231,131 @@ function isUtf8Text2(buffer) {
|
|
|
231
231
|
return false;
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
234
|
+
function getDirectiveTokens(content) {
|
|
235
|
+
return Array.from(content.matchAll(DIRECTIVE_RE2), (match) => ({
|
|
236
|
+
type: match[1],
|
|
237
|
+
condition: match[2],
|
|
238
|
+
index: match.index,
|
|
239
|
+
end: match.index + match[0].length
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
function parseNodes(content, tokens, tokenIndex, pos, stopTypes) {
|
|
243
|
+
const nodes = [];
|
|
244
|
+
while (tokenIndex < tokens.length) {
|
|
245
|
+
const token = tokens[tokenIndex];
|
|
246
|
+
if (stopTypes.includes(token.type)) {
|
|
247
|
+
if (token.index > pos)
|
|
248
|
+
nodes.push({ type: "text", text: content.slice(pos, token.index) });
|
|
249
|
+
return { nodes, pos: token.index, tokenIndex, stop: token };
|
|
250
|
+
}
|
|
251
|
+
if (token.type !== "if") {
|
|
252
|
+
throw new Error(`Unexpected <@${token.type}> without <@if>`);
|
|
253
|
+
}
|
|
254
|
+
if (token.index > pos)
|
|
255
|
+
nodes.push({ type: "text", text: content.slice(pos, token.index) });
|
|
256
|
+
const parsed = parseIfNode(content, tokens, tokenIndex);
|
|
257
|
+
nodes.push(parsed.node);
|
|
258
|
+
tokenIndex = parsed.tokenIndex;
|
|
259
|
+
pos = parsed.pos;
|
|
260
|
+
}
|
|
261
|
+
if (pos < content.length)
|
|
262
|
+
nodes.push({ type: "text", text: content.slice(pos) });
|
|
263
|
+
return { nodes, pos: content.length, tokenIndex };
|
|
264
|
+
}
|
|
265
|
+
function parseIfNode(content, tokens, tokenIndex) {
|
|
266
|
+
const firstToken = tokens[tokenIndex];
|
|
267
|
+
const sourceStart = firstToken.index;
|
|
268
|
+
const branches = [];
|
|
269
|
+
let branchType = "if";
|
|
270
|
+
let branchCondition = firstToken.condition;
|
|
271
|
+
let branchContentStart = firstToken.end;
|
|
272
|
+
tokenIndex += 1;
|
|
273
|
+
while (true) {
|
|
274
|
+
const parsed = parseNodes(content, tokens, tokenIndex, branchContentStart, ["elseif", "else", "endif"]);
|
|
275
|
+
if (!parsed.stop)
|
|
276
|
+
throw new Error("Unmatched <@if> without <@endif>");
|
|
277
|
+
branches.push({
|
|
278
|
+
type: branchType,
|
|
279
|
+
condition: branchCondition,
|
|
280
|
+
nodes: parsed.nodes,
|
|
281
|
+
contentStart: branchContentStart,
|
|
282
|
+
contentEnd: parsed.pos
|
|
283
|
+
});
|
|
284
|
+
if (parsed.stop.type === "endif") {
|
|
285
|
+
const sourceEnd = parsed.stop.end;
|
|
286
|
+
return {
|
|
287
|
+
node: {
|
|
288
|
+
type: "if",
|
|
289
|
+
sourceStart,
|
|
290
|
+
sourceEnd,
|
|
291
|
+
source: content.slice(sourceStart, sourceEnd),
|
|
292
|
+
branches
|
|
293
|
+
},
|
|
294
|
+
pos: sourceEnd,
|
|
295
|
+
tokenIndex: parsed.tokenIndex + 1
|
|
296
|
+
};
|
|
287
297
|
}
|
|
298
|
+
branchType = parsed.stop.type;
|
|
299
|
+
branchCondition = parsed.stop.condition;
|
|
300
|
+
branchContentStart = parsed.stop.end;
|
|
301
|
+
tokenIndex = parsed.tokenIndex + 1;
|
|
288
302
|
}
|
|
289
|
-
|
|
290
|
-
|
|
303
|
+
}
|
|
304
|
+
function getActiveBranch(node, context) {
|
|
305
|
+
for (const branch of node.branches) {
|
|
306
|
+
if (branch.type === "else" || evalCondition(branch.condition, context))
|
|
307
|
+
return branch;
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
function renderNodes(nodes, context, conditionalTokens, outputStart = 0) {
|
|
312
|
+
let result = "";
|
|
313
|
+
for (const node of nodes) {
|
|
314
|
+
if (node.type === "text") {
|
|
315
|
+
result += node.text;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const activeBranch = getActiveBranch(node, context);
|
|
319
|
+
const start = outputStart + result.length;
|
|
320
|
+
const renderedBranch = activeBranch ? renderNodes(activeBranch.nodes, context, conditionalTokens, start) : "";
|
|
321
|
+
result += renderedBranch;
|
|
322
|
+
conditionalTokens.push({
|
|
323
|
+
result: renderedBranch,
|
|
324
|
+
token: node.source,
|
|
325
|
+
range: { start, end: start + renderedBranch.length },
|
|
326
|
+
activeRange: activeBranch ? {
|
|
327
|
+
start: activeBranch.contentStart - node.sourceStart,
|
|
328
|
+
end: activeBranch.contentEnd - node.sourceStart
|
|
329
|
+
} : { start: node.source.length, end: node.source.length }
|
|
330
|
+
});
|
|
291
331
|
}
|
|
292
|
-
result += content.slice(pos);
|
|
293
332
|
return result;
|
|
294
333
|
}
|
|
334
|
+
function processIfBlocksWithMap(content, context) {
|
|
335
|
+
const tokens = getDirectiveTokens(content);
|
|
336
|
+
const parsed = parseNodes(content, tokens, 0, 0, []);
|
|
337
|
+
const conditionalTokens = [];
|
|
338
|
+
return {
|
|
339
|
+
content: renderNodes(parsed.nodes, context, conditionalTokens),
|
|
340
|
+
conditionalTokens
|
|
341
|
+
};
|
|
342
|
+
}
|
|
295
343
|
function renderContentWithMap(content, context, state, file) {
|
|
296
|
-
const
|
|
344
|
+
const processedResult = processIfBlocksWithMap(content, context);
|
|
345
|
+
const processed = processedResult.content;
|
|
346
|
+
for (const token of processedResult.conditionalTokens) {
|
|
347
|
+
addReverseMapToken(state, file, {
|
|
348
|
+
kind: "conditional",
|
|
349
|
+
result: token.result,
|
|
350
|
+
token: token.token,
|
|
351
|
+
outputPath: file?.outputPath ?? "",
|
|
352
|
+
templatePath: file?.templatePath ?? "",
|
|
353
|
+
range: token.range,
|
|
354
|
+
activeRange: token.activeRange,
|
|
355
|
+
before: processed.slice(0, token.range.start),
|
|
356
|
+
after: processed.slice(token.range.end)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
297
359
|
let result = "";
|
|
298
360
|
let pos = 0;
|
|
299
361
|
for (const match of processed.matchAll(VAR_RE2)) {
|
|
@@ -350,6 +412,37 @@ function validateOutputPaths(srcDir, destDir, context) {
|
|
|
350
412
|
}
|
|
351
413
|
}
|
|
352
414
|
}
|
|
415
|
+
function storeSkippedTemplate(srcPath, state) {
|
|
416
|
+
if (!state.manifest)
|
|
417
|
+
return;
|
|
418
|
+
const stat = statSync2(srcPath);
|
|
419
|
+
const templatePath = relative(state.sourceRoot, srcPath);
|
|
420
|
+
if (stat.isDirectory()) {
|
|
421
|
+
state.manifest.skipped.push({ kind: "directory", templatePath });
|
|
422
|
+
for (const entry of readdirSync2(srcPath).sort()) {
|
|
423
|
+
storeSkippedTemplate(resolvePath(srcPath, entry), state);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!stat.isFile())
|
|
428
|
+
return;
|
|
429
|
+
const content = readFileSync2(srcPath);
|
|
430
|
+
if (isUtf8Text2(content)) {
|
|
431
|
+
state.manifest.skipped.push({
|
|
432
|
+
kind: "file",
|
|
433
|
+
templatePath,
|
|
434
|
+
encoding: "utf8",
|
|
435
|
+
content: content.toString("utf-8")
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
state.manifest.skipped.push({
|
|
439
|
+
kind: "file",
|
|
440
|
+
templatePath,
|
|
441
|
+
encoding: "base64",
|
|
442
|
+
content: content.toString("base64")
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
353
446
|
function renderDirInner(srcDir, destDir, context, state) {
|
|
354
447
|
mkdirSync(destDir, { recursive: true });
|
|
355
448
|
const entries = readdirSync2(srcDir).sort();
|
|
@@ -363,8 +456,10 @@ function renderDirInner(srcDir, destDir, context, state) {
|
|
|
363
456
|
tokens: []
|
|
364
457
|
};
|
|
365
458
|
const outputName = getOutputName(entry, context, state, tempFile);
|
|
366
|
-
if (outputName === null)
|
|
459
|
+
if (outputName === null) {
|
|
460
|
+
storeSkippedTemplate(srcPath, state);
|
|
367
461
|
continue;
|
|
462
|
+
}
|
|
368
463
|
const destPath = resolveOutputPath(destDir, outputName);
|
|
369
464
|
const outputPath = relative(state.destRoot, destPath);
|
|
370
465
|
tempFile.outputPath = outputPath;
|
|
@@ -385,8 +480,8 @@ function renderDirInner(srcDir, destDir, context, state) {
|
|
|
385
480
|
} else {
|
|
386
481
|
copyFileSync(srcPath, destPath);
|
|
387
482
|
}
|
|
388
|
-
if (
|
|
389
|
-
state.manifest
|
|
483
|
+
if (state.manifest)
|
|
484
|
+
state.manifest.files.push(tempFile);
|
|
390
485
|
}
|
|
391
486
|
}
|
|
392
487
|
}
|
|
@@ -446,10 +541,66 @@ function replaceFirst(content, token) {
|
|
|
446
541
|
return null;
|
|
447
542
|
return `${content.slice(0, index)}${token.token}${content.slice(index + token.result.length)}`;
|
|
448
543
|
}
|
|
544
|
+
function applyActiveBranch(token, branchContent) {
|
|
545
|
+
if (!token.activeRange)
|
|
546
|
+
return token.token;
|
|
547
|
+
return [
|
|
548
|
+
token.token.slice(0, token.activeRange.start),
|
|
549
|
+
branchContent,
|
|
550
|
+
token.token.slice(token.activeRange.end)
|
|
551
|
+
].join("");
|
|
552
|
+
}
|
|
553
|
+
function findPrefixEnd(content, before) {
|
|
554
|
+
if (before === "")
|
|
555
|
+
return 0;
|
|
556
|
+
let candidate = before;
|
|
557
|
+
while (candidate.length >= 8) {
|
|
558
|
+
const index = content.indexOf(candidate);
|
|
559
|
+
if (index !== -1)
|
|
560
|
+
return index + candidate.length;
|
|
561
|
+
candidate = candidate.slice(-Math.max(1, Math.floor(candidate.length / 2)));
|
|
562
|
+
}
|
|
563
|
+
return -1;
|
|
564
|
+
}
|
|
565
|
+
function findSuffixStart(content, after, from) {
|
|
566
|
+
if (after === "")
|
|
567
|
+
return content.length;
|
|
568
|
+
let candidate = after;
|
|
569
|
+
while (candidate.length >= 8) {
|
|
570
|
+
const index = content.indexOf(candidate, from);
|
|
571
|
+
if (index !== -1)
|
|
572
|
+
return index;
|
|
573
|
+
candidate = candidate.slice(0, Math.floor(candidate.length / 2));
|
|
574
|
+
}
|
|
575
|
+
return -1;
|
|
576
|
+
}
|
|
577
|
+
function replaceConditional(content, token) {
|
|
578
|
+
const exactIndex = content.indexOf(token.result);
|
|
579
|
+
if (exactIndex !== -1) {
|
|
580
|
+
return [
|
|
581
|
+
content.slice(0, exactIndex),
|
|
582
|
+
token.token,
|
|
583
|
+
content.slice(exactIndex + token.result.length)
|
|
584
|
+
].join("");
|
|
585
|
+
}
|
|
586
|
+
if (token.before === undefined || token.after === undefined)
|
|
587
|
+
return null;
|
|
588
|
+
const branchStart = findPrefixEnd(content, token.before);
|
|
589
|
+
if (branchStart === -1)
|
|
590
|
+
return null;
|
|
591
|
+
const afterIndex = findSuffixStart(content, token.after, branchStart);
|
|
592
|
+
if (afterIndex === -1)
|
|
593
|
+
return null;
|
|
594
|
+
return [
|
|
595
|
+
content.slice(0, branchStart),
|
|
596
|
+
applyActiveBranch(token, content.slice(branchStart, afterIndex)),
|
|
597
|
+
content.slice(afterIndex)
|
|
598
|
+
].join("");
|
|
599
|
+
}
|
|
449
600
|
function reverseContent(content, file, warnings) {
|
|
450
|
-
const
|
|
601
|
+
const contentTokens = file.tokens.filter((token) => token.kind === "content").sort((a, b) => (b.range?.start ?? -1) - (a.range?.start ?? -1));
|
|
451
602
|
let reversed = content;
|
|
452
|
-
for (const token of
|
|
603
|
+
for (const token of contentTokens) {
|
|
453
604
|
const rangeResult = replaceAtRange(reversed, token);
|
|
454
605
|
if (rangeResult !== null) {
|
|
455
606
|
reversed = rangeResult;
|
|
@@ -467,8 +618,33 @@ function reverseContent(content, file, warnings) {
|
|
|
467
618
|
message: "Rendered value was not found; token was not restored"
|
|
468
619
|
});
|
|
469
620
|
}
|
|
621
|
+
const conditionalTokens = file.tokens.filter((token) => token.kind === "conditional").sort((a, b) => (b.range?.start ?? -1) - (a.range?.start ?? -1));
|
|
622
|
+
for (const token of conditionalTokens) {
|
|
623
|
+
const result = replaceConditional(reversed, token);
|
|
624
|
+
if (result !== null) {
|
|
625
|
+
reversed = result;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
warnings.push({
|
|
629
|
+
outputPath: file.outputPath,
|
|
630
|
+
token: token.token,
|
|
631
|
+
result: token.result,
|
|
632
|
+
message: "Rendered conditional block was not found; block was not restored"
|
|
633
|
+
});
|
|
634
|
+
}
|
|
470
635
|
return reversed;
|
|
471
636
|
}
|
|
637
|
+
function writeSkippedTemplate(templateRoot, skipped) {
|
|
638
|
+
const templatePath = resolveInside(templateRoot, skipped.templatePath);
|
|
639
|
+
if (skipped.kind === "directory") {
|
|
640
|
+
mkdirSync2(templatePath, { recursive: true });
|
|
641
|
+
return 0;
|
|
642
|
+
}
|
|
643
|
+
mkdirSync2(dirname2(templatePath), { recursive: true });
|
|
644
|
+
const content = skipped.encoding === "base64" ? Buffer.from(skipped.content ?? "", "base64") : skipped.content ?? "";
|
|
645
|
+
writeFileSync2(templatePath, content);
|
|
646
|
+
return 1;
|
|
647
|
+
}
|
|
472
648
|
function reverseDir(renderedDir, templateDir, options = {}) {
|
|
473
649
|
const renderedRoot = resolvePath2(renderedDir);
|
|
474
650
|
const templateRoot = resolvePath2(templateDir);
|
|
@@ -505,6 +681,9 @@ function reverseDir(renderedDir, templateDir, options = {}) {
|
|
|
505
681
|
writeFileSync2(templatePath, reverseContent(content.toString("utf-8"), file, warnings));
|
|
506
682
|
filesWritten += 1;
|
|
507
683
|
}
|
|
684
|
+
for (const skipped of manifest.skipped ?? []) {
|
|
685
|
+
filesWritten += writeSkippedTemplate(templateRoot, skipped);
|
|
686
|
+
}
|
|
508
687
|
return { filesWritten, warnings };
|
|
509
688
|
}
|
|
510
689
|
|
package/dist/render.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type ReverseMapToken = {
|
|
2
|
-
kind: "path" | "content";
|
|
2
|
+
kind: "path" | "content" | "conditional";
|
|
3
3
|
result: string;
|
|
4
4
|
token: string;
|
|
5
5
|
contextPath?: string;
|
|
@@ -9,15 +9,28 @@ export type ReverseMapToken = {
|
|
|
9
9
|
start: number;
|
|
10
10
|
end: number;
|
|
11
11
|
};
|
|
12
|
+
activeRange?: {
|
|
13
|
+
start: number;
|
|
14
|
+
end: number;
|
|
15
|
+
};
|
|
16
|
+
before?: string;
|
|
17
|
+
after?: string;
|
|
12
18
|
};
|
|
13
19
|
export type ReverseMapFile = {
|
|
14
20
|
outputPath: string;
|
|
15
21
|
templatePath: string;
|
|
16
22
|
tokens: ReverseMapToken[];
|
|
17
23
|
};
|
|
24
|
+
export type ReverseMapStoredTemplate = {
|
|
25
|
+
kind: "directory" | "file";
|
|
26
|
+
templatePath: string;
|
|
27
|
+
encoding?: "utf8" | "base64";
|
|
28
|
+
content?: string;
|
|
29
|
+
};
|
|
18
30
|
export type ReverseMapManifest = {
|
|
19
31
|
version: 1;
|
|
20
32
|
files: ReverseMapFile[];
|
|
33
|
+
skipped: ReverseMapStoredTemplate[];
|
|
21
34
|
tokens: Record<string, string[]>;
|
|
22
35
|
};
|
|
23
36
|
export type RenderOptions = {
|