@gregorlohaus/tdir 0.1.7 → 0.1.9
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 +6 -0
- package/dist/index.js +133 -24
- package/dist/scanner.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,9 @@ render("./output", {
|
|
|
60
60
|
|---|---|
|
|
61
61
|
| `<@if(context.x)>` | Conditional block — boolean check (must end with `<@endif>`) |
|
|
62
62
|
| `<@if(eq(context.x,"value"))>` | Conditional block — string equality check |
|
|
63
|
+
| `<@if(neq(context.x,"value"))>` | Conditional block — string inequality check |
|
|
64
|
+
| `<@if(and(context.x,eq(context.y,"value")))>` | Conditional block — all child conditions must match |
|
|
65
|
+
| `<@if(or(context.x,eq(context.y,"value")))>` | Conditional block — any child condition may match |
|
|
63
66
|
| `<@elseif(context.y)>` | Else-if branch (same forms as `@if`) |
|
|
64
67
|
| `<@else>` | Else branch |
|
|
65
68
|
| `<@endif>` | End conditional block |
|
|
@@ -72,6 +75,9 @@ render("./output", {
|
|
|
72
75
|
|---|---|
|
|
73
76
|
| `<@if(context.x)>dirname` | Conditionally include directory/file (boolean check) |
|
|
74
77
|
| `<@if(eq(context.x,"value"))>dirname` | Conditionally include by string equality |
|
|
78
|
+
| `<@if(neq(context.x,"value"))>dirname` | Conditionally include by string inequality |
|
|
79
|
+
| `<@if(and(context.x,eq(context.y,"value")))>dirname` | Conditionally include by combined conditions |
|
|
80
|
+
| `<@if(or(context.x,eq(context.y,"value")))>dirname` | Conditionally include by alternate conditions |
|
|
75
81
|
| `<@var(context.x)>` | Dynamic directory/file name |
|
|
76
82
|
|
|
77
83
|
These can be combined: `<@if(context.web.create)><@var(context.web.dir)>` creates a directory named by `context.web.dir` only if `context.web.create` is true.
|
package/dist/index.js
CHANGED
|
@@ -5,17 +5,123 @@ import { z } from "zod";
|
|
|
5
5
|
import { readdirSync, statSync, readFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { TextDecoder } from "node:util";
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
// scanner.ts
|
|
10
|
+
function readCondition(text, start) {
|
|
11
|
+
let depth = 0;
|
|
12
|
+
let inString = false;
|
|
13
|
+
let escaped = false;
|
|
14
|
+
for (let i = start;i < text.length; i++) {
|
|
15
|
+
const char = text[i];
|
|
16
|
+
if (escaped) {
|
|
17
|
+
escaped = false;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (char === "\\") {
|
|
21
|
+
escaped = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (char === '"') {
|
|
25
|
+
inString = !inString;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (inString)
|
|
29
|
+
continue;
|
|
30
|
+
if (char === "(") {
|
|
31
|
+
depth += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (char === ")") {
|
|
35
|
+
depth -= 1;
|
|
36
|
+
if (depth === 0 && text[i + 1] === ">") {
|
|
37
|
+
return { condition: text.slice(start + 1, i), end: i + 2 };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function getDirectiveTokens(text) {
|
|
44
|
+
const tokens = [];
|
|
45
|
+
for (let i = 0;i < text.length; i++) {
|
|
46
|
+
if (text[i] !== "<" || text[i + 1] !== "@")
|
|
47
|
+
continue;
|
|
48
|
+
const rest = text.slice(i + 2);
|
|
49
|
+
const type = ["elseif", "endif", "else", "if"].find((name) => rest.startsWith(name));
|
|
50
|
+
if (!type)
|
|
51
|
+
continue;
|
|
52
|
+
const afterName = i + 2 + type.length;
|
|
53
|
+
if ((type === "if" || type === "elseif") && text[afterName] === "(") {
|
|
54
|
+
const parsed = readCondition(text, afterName);
|
|
55
|
+
if (!parsed)
|
|
56
|
+
continue;
|
|
57
|
+
tokens.push({ type, condition: parsed.condition, index: i, end: parsed.end });
|
|
58
|
+
i = parsed.end - 1;
|
|
59
|
+
} else if ((type === "else" || type === "endif") && text[afterName] === ">") {
|
|
60
|
+
tokens.push({ type, index: i, end: afterName + 1 });
|
|
61
|
+
i = afterName;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return tokens;
|
|
65
|
+
}
|
|
66
|
+
function splitArgs(args) {
|
|
67
|
+
const result = [];
|
|
68
|
+
let current = "";
|
|
69
|
+
let depth = 0;
|
|
70
|
+
let inString = false;
|
|
71
|
+
let escaped = false;
|
|
72
|
+
for (const char of args) {
|
|
73
|
+
if (escaped) {
|
|
74
|
+
current += char;
|
|
75
|
+
escaped = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (char === "\\") {
|
|
79
|
+
current += char;
|
|
80
|
+
escaped = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (char === '"') {
|
|
84
|
+
current += char;
|
|
85
|
+
inString = !inString;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!inString && char === "(")
|
|
89
|
+
depth += 1;
|
|
90
|
+
if (!inString && char === ")")
|
|
91
|
+
depth -= 1;
|
|
92
|
+
if (!inString && depth === 0 && char === ",") {
|
|
93
|
+
result.push(current.trim());
|
|
94
|
+
current = "";
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
current += char;
|
|
98
|
+
}
|
|
99
|
+
if (current.trim() !== "")
|
|
100
|
+
result.push(current.trim());
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// parser.ts
|
|
9
105
|
var VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g;
|
|
10
|
-
var
|
|
11
|
-
var EQ_RE = /^eq\(context\.(.+?),\s*"(.*)"\)$/;
|
|
106
|
+
var STRING_COMPARE_RE = /^(?:eq|neq)\(context\.(.+?),\s*"(.*)"\)$/;
|
|
12
107
|
var PATH_RE = /^context\.(.+)$/;
|
|
13
108
|
function extractCondition(expr, vars) {
|
|
14
109
|
if (!expr)
|
|
15
110
|
throw new Error("Missing condition expression");
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
111
|
+
for (const operator of ["and", "or"]) {
|
|
112
|
+
const prefix = `${operator}(`;
|
|
113
|
+
if (expr.startsWith(prefix) && expr.endsWith(")")) {
|
|
114
|
+
const args = splitArgs(expr.slice(prefix.length, -1));
|
|
115
|
+
if (args.length === 0)
|
|
116
|
+
throw new Error(`Invalid condition expression: ${expr}`);
|
|
117
|
+
for (const arg of args)
|
|
118
|
+
extractCondition(arg, vars);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const stringCompareMatch = expr.match(STRING_COMPARE_RE);
|
|
123
|
+
if (stringCompareMatch) {
|
|
124
|
+
vars.push({ path: stringCompareMatch[1], type: "string" });
|
|
19
125
|
return;
|
|
20
126
|
}
|
|
21
127
|
const pathMatch = expr.match(PATH_RE);
|
|
@@ -26,8 +132,10 @@ function extractCondition(expr, vars) {
|
|
|
26
132
|
throw new Error(`Invalid condition expression: ${expr}`);
|
|
27
133
|
}
|
|
28
134
|
function extractFromString(text, vars) {
|
|
29
|
-
for (const
|
|
30
|
-
|
|
135
|
+
for (const token of getDirectiveTokens(text)) {
|
|
136
|
+
if (token.type === "if" || token.type === "elseif") {
|
|
137
|
+
extractCondition(token.condition, vars);
|
|
138
|
+
}
|
|
31
139
|
}
|
|
32
140
|
for (const match of text.matchAll(VAR_RE)) {
|
|
33
141
|
vars.push({ path: match[1], type: match[2] ?? "string" });
|
|
@@ -35,9 +143,9 @@ function extractFromString(text, vars) {
|
|
|
35
143
|
}
|
|
36
144
|
function validateIfBlocks(content, vars) {
|
|
37
145
|
const stack = [];
|
|
38
|
-
for (const
|
|
39
|
-
const directive =
|
|
40
|
-
const condition =
|
|
146
|
+
for (const token of getDirectiveTokens(content)) {
|
|
147
|
+
const directive = token.type;
|
|
148
|
+
const condition = token.condition;
|
|
41
149
|
if (directive === "if") {
|
|
42
150
|
extractCondition(condition, vars);
|
|
43
151
|
stack.push({ sawElse: false });
|
|
@@ -121,15 +229,24 @@ import { homedir } from "node:os";
|
|
|
121
229
|
import { TextDecoder as TextDecoder2 } from "node:util";
|
|
122
230
|
var IF_PATH_RE = /^<@if\((.+?)\)>(.*)$/;
|
|
123
231
|
var VAR_RE2 = /<@var\(context\.(.+?)(?::(\w+))?\)>/g;
|
|
124
|
-
var
|
|
125
|
-
var EQ_RE2 = /^eq\(context\.(.+?),\s*"(.*)"\)$/;
|
|
232
|
+
var STRING_COMPARE_RE2 = /^(eq|neq)\(context\.(.+?),\s*"(.*)"\)$/;
|
|
126
233
|
var PATH_RE2 = /^context\.(.+)$/;
|
|
127
234
|
function evalCondition(expr, context) {
|
|
128
235
|
if (!expr)
|
|
129
236
|
throw new Error("Missing condition expression");
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
237
|
+
for (const operator of ["and", "or"]) {
|
|
238
|
+
const prefix = `${operator}(`;
|
|
239
|
+
if (expr.startsWith(prefix) && expr.endsWith(")")) {
|
|
240
|
+
const args = splitArgs(expr.slice(prefix.length, -1));
|
|
241
|
+
if (args.length === 0)
|
|
242
|
+
throw new Error(`Invalid condition expression: ${expr}`);
|
|
243
|
+
return operator === "and" ? args.every((arg) => evalCondition(arg, context)) : args.some((arg) => evalCondition(arg, context));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const stringCompareMatch = expr.match(STRING_COMPARE_RE2);
|
|
247
|
+
if (stringCompareMatch) {
|
|
248
|
+
const result = resolveContext(context, stringCompareMatch[2]) === stringCompareMatch[3];
|
|
249
|
+
return stringCompareMatch[1] === "eq" ? result : !result;
|
|
133
250
|
}
|
|
134
251
|
const pathMatch = expr.match(PATH_RE2);
|
|
135
252
|
if (pathMatch) {
|
|
@@ -239,14 +356,6 @@ function isUtf8Text2(buffer) {
|
|
|
239
356
|
return false;
|
|
240
357
|
}
|
|
241
358
|
}
|
|
242
|
-
function getDirectiveTokens(content) {
|
|
243
|
-
return Array.from(content.matchAll(DIRECTIVE_RE2), (match) => ({
|
|
244
|
-
type: match[1],
|
|
245
|
-
condition: match[2],
|
|
246
|
-
index: match.index,
|
|
247
|
-
end: match.index + match[0].length
|
|
248
|
-
}));
|
|
249
|
-
}
|
|
250
359
|
function parseNodes(content, tokens, tokenIndex, pos, stopTypes) {
|
|
251
360
|
const nodes = [];
|
|
252
361
|
while (tokenIndex < tokens.length) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type DirectiveToken = {
|
|
2
|
+
type: "if" | "elseif" | "else" | "endif";
|
|
3
|
+
condition?: string;
|
|
4
|
+
index: number;
|
|
5
|
+
end: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function getDirectiveTokens(text: string): DirectiveToken[];
|
|
8
|
+
export declare function splitArgs(args: string): string[];
|