@zvndev/darkmown 2.1.0 → 2.3.1-next.1.g3a4a8dc
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/CHANGELOG.md +22 -0
- package/README.md +65 -0
- package/docs/cli.md +8 -0
- package/docs/spec-alignment.md +18 -0
- package/grammar/wd-directives.gbnf +58 -0
- package/package.json +17 -10
- package/site/pages/docs/index.wd +29 -0
- package/site/pages/playground/index.js +47 -46
- package/src/builder.js +5 -0
- package/src/catalog.js +601 -0
- package/src/cli.js +8 -0
- package/src/compiler/actions.js +87 -34
- package/src/compiler/body.js +36 -14
- package/src/compiler/collections.js +16 -10
- package/src/compiler/context.js +70 -1
- package/src/compiler/fetch.js +51 -19
- package/src/compiler/format.js +5 -2
- package/src/compiler/forms.js +169 -50
- package/src/compiler/frontmatter.js +11 -2
- package/src/compiler/fs-reader.js +2 -1
- package/src/compiler/image-size.js +340 -0
- package/src/compiler/includes.js +8 -4
- package/src/compiler/interpolation.js +15 -7
- package/src/compiler/loops.js +78 -39
- package/src/compiler/markdown.js +4 -3
- package/src/compiler/media.js +38 -10
- package/src/compiler/page.js +8 -6
- package/src/compiler/predicates.js +74 -30
- package/src/compiler/reader.js +12 -2
- package/src/compiler/state.js +83 -18
- package/src/compiler/structure.js +57 -24
- package/src/deploy.js +8 -4
- package/src/errors.js +1156 -0
- package/src/grammar.js +131 -0
- package/src/index.js +1 -0
- package/src/router.js +7 -4
- package/src/scaffold.js +4 -2
- package/src/skin.js +17 -6
- package/types/api-runner.d.ts +94 -0
- package/types/builder.d.ts +141 -10
- package/types/catalog.d.ts +97 -0
- package/types/compiler/actions.d.ts +41 -0
- package/types/compiler/body.d.ts +11 -0
- package/types/compiler/collections.d.ts +76 -0
- package/types/compiler/context.d.ts +647 -0
- package/types/compiler/directives.d.ts +6 -0
- package/types/compiler/expr-ast.d.ts +20 -0
- package/types/compiler/fetch.d.ts +29 -0
- package/types/compiler/format.d.ts +49 -0
- package/types/compiler/forms.d.ts +94 -0
- package/types/compiler/frontmatter.d.ts +33 -0
- package/types/compiler/fs-reader.d.ts +7 -0
- package/types/compiler/image-size.d.ts +14 -0
- package/types/compiler/includes.d.ts +100 -0
- package/types/compiler/index.d.ts +5 -0
- package/types/compiler/interpolation.d.ts +115 -0
- package/types/compiler/loops.d.ts +36 -0
- package/types/compiler/markdown.d.ts +34 -0
- package/types/compiler/media.d.ts +32 -0
- package/types/compiler/page.d.ts +109 -0
- package/types/compiler/predicates.d.ts +50 -0
- package/types/compiler/reader.d.ts +48 -0
- package/types/compiler/state.d.ts +68 -0
- package/types/compiler/structure.d.ts +56 -0
- package/types/compiler.d.ts +35 -223
- package/types/dev.d.ts +47 -2
- package/types/errors.d.ts +100 -0
- package/types/feeds.d.ts +150 -0
- package/types/headers.d.ts +44 -0
- package/types/highlight.d.ts +24 -0
- package/types/router.d.ts +42 -3
- package/types/scaffold.d.ts +15 -3
- package/types/skin.d.ts +28 -1
- package/types/src/api-runner.d.ts +94 -0
- package/types/src/builder.d.ts +170 -0
- package/types/src/catalog.d.ts +97 -0
- package/types/src/compiler/actions.d.ts +41 -0
- package/types/src/compiler/body.d.ts +11 -0
- package/types/src/compiler/collections.d.ts +76 -0
- package/types/src/compiler/context.d.ts +647 -0
- package/types/src/compiler/directives.d.ts +6 -0
- package/types/src/compiler/expr-ast.d.ts +20 -0
- package/types/src/compiler/fetch.d.ts +29 -0
- package/types/src/compiler/format.d.ts +49 -0
- package/types/src/compiler/forms.d.ts +94 -0
- package/types/src/compiler/frontmatter.d.ts +33 -0
- package/types/src/compiler/fs-reader.d.ts +7 -0
- package/types/src/compiler/image-size.d.ts +14 -0
- package/types/src/compiler/includes.d.ts +100 -0
- package/types/src/compiler/index.d.ts +5 -0
- package/types/src/compiler/interpolation.d.ts +115 -0
- package/types/src/compiler/loops.d.ts +36 -0
- package/types/src/compiler/markdown.d.ts +34 -0
- package/types/src/compiler/media.d.ts +32 -0
- package/types/src/compiler/page.d.ts +109 -0
- package/types/src/compiler/predicates.d.ts +50 -0
- package/types/src/compiler/reader.d.ts +48 -0
- package/types/src/compiler/state.d.ts +68 -0
- package/types/src/compiler/structure.d.ts +56 -0
- package/types/src/compiler.d.ts +48 -0
- package/types/src/config.d.ts +45 -0
- package/types/src/dev.d.ts +58 -0
- package/types/src/errors.d.ts +100 -0
- package/types/src/feeds.d.ts +150 -0
- package/types/src/headers.d.ts +44 -0
- package/types/src/highlight.d.ts +24 -0
- package/types/src/router.d.ts +78 -0
- package/types/src/scaffold.d.ts +20 -0
- package/types/src/skin.d.ts +48 -0
- package/types/src/statics.d.ts +24 -0
- package/types/statics.d.ts +2 -1
package/src/compiler/actions.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// while keeping the message text and `Use:` hints intact.
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
|
|
12
|
-
import { at } from "./context.js";
|
|
12
|
+
import { at, lineOf, wdError } from "./context.js";
|
|
13
13
|
import {
|
|
14
14
|
escapeHtml,
|
|
15
15
|
resolveStateKey,
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
* @typedef {import("./context.js").Action} Action
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
// A concrete, compilable :button line for the malformed hint + directive catalog.
|
|
27
|
+
export const BUTTON_EXAMPLE = ':button "Add one" -> count++';
|
|
28
|
+
const BUTTON_USE = `Use: :button "Label" -> action — e.g. ${BUTTON_EXAMPLE}`;
|
|
29
|
+
|
|
26
30
|
/**
|
|
27
31
|
* @param {string} line
|
|
28
32
|
* @param {Ctx} ctx
|
|
@@ -31,7 +35,14 @@ import {
|
|
|
31
35
|
*/
|
|
32
36
|
export function handleButton(line, ctx, index) {
|
|
33
37
|
const match = line.match(/^:button\s+"([^"]+)"\s*->\s*(.+)$/);
|
|
34
|
-
if (!match)
|
|
38
|
+
if (!match)
|
|
39
|
+
throw wdError(`Malformed :button in ${at(ctx, index)}: ${line}. ${BUTTON_USE}`, {
|
|
40
|
+
code: "WD301",
|
|
41
|
+
file: ctx.file,
|
|
42
|
+
line: lineOf(ctx, index),
|
|
43
|
+
hint: BUTTON_USE.slice("Use: ".length),
|
|
44
|
+
example: BUTTON_EXAMPLE
|
|
45
|
+
});
|
|
35
46
|
ctx.comp.assets.runtime = true;
|
|
36
47
|
const action = parseAction(match[2], ctx);
|
|
37
48
|
if (Array.isArray(action)) {
|
|
@@ -44,8 +55,11 @@ export function handleButton(line, ctx, index) {
|
|
|
44
55
|
return `<button type="button" data-wd-action="${action.op}" data-wd-target="${action.target}"${valueAttr}>${escapeHtml(match[1])}</button>`;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
// Concrete, compilable examples for the effect / every hint tails + catalog.
|
|
59
|
+
// (Effects run the :button action vocabulary — mutations, not expressions; use
|
|
60
|
+
// :computed for derived values. `searches++` is the canonical watch-and-act.)
|
|
61
|
+
export const EFFECT_EXAMPLE = ":effect query -> searches++";
|
|
62
|
+
const EFFECT_USE = `Use: :effect watchedState -> action[; action…] (actions use the :button vocabulary) — e.g. ${EFFECT_EXAMPLE}`;
|
|
49
63
|
|
|
50
64
|
/**
|
|
51
65
|
* Parse `:effect <watched> -> <actions>` into a zero-output marker the runtime
|
|
@@ -59,12 +73,20 @@ const EFFECT_USE =
|
|
|
59
73
|
*/
|
|
60
74
|
export function handleEffect(line, ctx, index) {
|
|
61
75
|
const match = line.match(/^:effect\s+([A-Za-z_$][\w$.]*)\s*->\s*(.+)$/);
|
|
62
|
-
if (!match)
|
|
76
|
+
if (!match)
|
|
77
|
+
throw wdError(`Malformed :effect in ${at(ctx, index)}: ${line}. ${EFFECT_USE}`, {
|
|
78
|
+
code: "WD302",
|
|
79
|
+
file: ctx.file,
|
|
80
|
+
line: lineOf(ctx, index),
|
|
81
|
+
hint: EFFECT_USE.slice("Use: ".length),
|
|
82
|
+
example: EFFECT_EXAMPLE
|
|
83
|
+
});
|
|
63
84
|
const segs = validatePath(match[1], ctx, EFFECT_USE);
|
|
64
85
|
const key = resolveStateKey(segs[0], ctx);
|
|
65
86
|
if (!key)
|
|
66
|
-
throw
|
|
67
|
-
`:effect watches unknown state "${segs[0]}" in ${ctx.file}. Declare it first with :state ${segs[0]} =
|
|
87
|
+
throw wdError(
|
|
88
|
+
`:effect watches unknown state "${segs[0]}" in ${ctx.file}. Declare it first with :state ${segs[0]} = ...`,
|
|
89
|
+
{ code: "WD303", file: ctx.file }
|
|
68
90
|
);
|
|
69
91
|
ctx.comp.assets.runtime = true;
|
|
70
92
|
const watch = [key, ...segs.slice(1)].join(".");
|
|
@@ -73,8 +95,8 @@ export function handleEffect(line, ctx, index) {
|
|
|
73
95
|
return `<script type="application/json" data-wd-effect>${safeScriptJson({ watch, actions })}</script>`;
|
|
74
96
|
}
|
|
75
97
|
|
|
76
|
-
const
|
|
77
|
-
|
|
98
|
+
export const EVERY_EXAMPLE = ":every 5s -> seconds++";
|
|
99
|
+
const EVERY_USE = `Use: :every <duration> -> action[; action…] — duration like 5s, 500ms, or 2m (actions use the :button vocabulary) — e.g. ${EVERY_EXAMPLE}`;
|
|
78
100
|
|
|
79
101
|
/**
|
|
80
102
|
* Parse a duration token into milliseconds: `<int>[ms|s|m]`, defaulting to ms.
|
|
@@ -101,12 +123,21 @@ function parseDuration(raw) {
|
|
|
101
123
|
*/
|
|
102
124
|
export function handleEvery(line, ctx, index) {
|
|
103
125
|
const match = line.match(/^:every\s+(\S+)\s*->\s*(.+)$/);
|
|
104
|
-
if (!match)
|
|
126
|
+
if (!match)
|
|
127
|
+
throw wdError(`Malformed :every in ${at(ctx, index)}: ${line}. ${EVERY_USE}`, {
|
|
128
|
+
code: "WD304",
|
|
129
|
+
file: ctx.file,
|
|
130
|
+
line: lineOf(ctx, index),
|
|
131
|
+
hint: EVERY_USE.slice("Use: ".length),
|
|
132
|
+
example: EVERY_EXAMPLE
|
|
133
|
+
});
|
|
105
134
|
const ms = parseDuration(match[1]);
|
|
106
135
|
if (ms == null || ms <= 0) {
|
|
107
|
-
throw
|
|
108
|
-
|
|
109
|
-
|
|
136
|
+
throw wdError(`:every duration "${match[1]}" is not valid in ${at(ctx, index)}. ${EVERY_USE}`, {
|
|
137
|
+
code: "WD305",
|
|
138
|
+
file: ctx.file,
|
|
139
|
+
line: lineOf(ctx, index)
|
|
140
|
+
});
|
|
110
141
|
}
|
|
111
142
|
ctx.comp.assets.runtime = true;
|
|
112
143
|
const action = parseAction(match[2], ctx);
|
|
@@ -114,8 +145,11 @@ export function handleEvery(line, ctx, index) {
|
|
|
114
145
|
return `<script type="application/json" data-wd-every>${safeScriptJson({ ms, actions })}</script>`;
|
|
115
146
|
}
|
|
116
147
|
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
// One concrete action expression, appended so a model copies `count++` rather
|
|
149
|
+
// than the schematic `name++`. The action vocabulary the directive catalog draws
|
|
150
|
+
// from lives here (the parser branches below), mirrored compactly in ACTION_USE.
|
|
151
|
+
export const ACTION_EXAMPLE = "count++";
|
|
152
|
+
export const ACTION_USE = `Use: name++, name--, n += k, n -= k, name = v, flag toggle, list append/prepend v, list toggle v, list remove v, x clear, obj merge other, obj delete key, name reset — chain with ";" — e.g. ${ACTION_EXAMPLE}`;
|
|
119
153
|
|
|
120
154
|
/**
|
|
121
155
|
* Parse a `:button` action expression into a validated `{ op, target, value }`,
|
|
@@ -148,8 +182,9 @@ function parseSingleAction(expression, raw, ctx) {
|
|
|
148
182
|
const segs = validatePath(name, ctx, ACTION_USE);
|
|
149
183
|
const key = resolveStateKey(segs[0], ctx);
|
|
150
184
|
if (!key) {
|
|
151
|
-
throw
|
|
152
|
-
`Button action targets unknown state "${segs[0]}" in ${ctx.file}. Declare it first with :state ${segs[0]} =
|
|
185
|
+
throw wdError(
|
|
186
|
+
`Button action targets unknown state "${segs[0]}" in ${ctx.file}. Declare it first with :state ${segs[0]} = ...`,
|
|
187
|
+
{ code: "WD306", file: ctx.file }
|
|
153
188
|
);
|
|
154
189
|
}
|
|
155
190
|
return [key, ...segs.slice(1)].join(".");
|
|
@@ -170,8 +205,9 @@ function parseSingleAction(expression, raw, ctx) {
|
|
|
170
205
|
if (ctx.loopItem && rhs === ctx.loopItem) {
|
|
171
206
|
const target = resolveTarget(add[1]);
|
|
172
207
|
if (!Array.isArray(ctx.comp.state.get(target))) {
|
|
173
|
-
throw
|
|
174
|
-
`Button action "${raw}" needs ${add[1]} to be a :state list (declare it "${add[1]} = []") in ${ctx.file}
|
|
208
|
+
throw wdError(
|
|
209
|
+
`Button action "${raw}" needs ${add[1]} to be a :state list (declare it "${add[1]} = []") in ${ctx.file}.`,
|
|
210
|
+
{ code: "WD307", file: ctx.file }
|
|
175
211
|
);
|
|
176
212
|
}
|
|
177
213
|
return { op: "append-row", target };
|
|
@@ -180,8 +216,9 @@ function parseSingleAction(expression, raw, ctx) {
|
|
|
180
216
|
const value = parseActionLiteral(rhs, ctx);
|
|
181
217
|
if (Array.isArray(ctx.comp.state.get(target))) return { op: "append", target, value };
|
|
182
218
|
if (typeof value === "number") return { op: "add", target, value };
|
|
183
|
-
throw
|
|
184
|
-
`Unsupported button action "${raw}" in ${ctx.file}. += with non-number values requires a list state target — declare it "${add[1]} = []"
|
|
219
|
+
throw wdError(
|
|
220
|
+
`Unsupported button action "${raw}" in ${ctx.file}. += with non-number values requires a list state target — declare it "${add[1]} = []".`,
|
|
221
|
+
{ code: "WD308", file: ctx.file }
|
|
185
222
|
);
|
|
186
223
|
}
|
|
187
224
|
// `flag toggle` (no operand) → boolean flip; `list toggle v` → member-toggle.
|
|
@@ -207,13 +244,15 @@ function parseSingleAction(expression, raw, ctx) {
|
|
|
207
244
|
// its source is a path off the outer row, which the runtime's row-remove
|
|
208
245
|
// can't target. Fail loud with the honest workaround.
|
|
209
246
|
if (!ctx.loopKey) {
|
|
210
|
-
throw
|
|
211
|
-
`Button action "${raw}" can't delete a row of a nested (item-relative) loop in ${ctx.file}. Per-row "remove" needs a top-level :state/:store list; carry the row into one (cart += ${ctx.loopItem}) and remove it there
|
|
247
|
+
throw wdError(
|
|
248
|
+
`Button action "${raw}" can't delete a row of a nested (item-relative) loop in ${ctx.file}. Per-row "remove" needs a top-level :state/:store list; carry the row into one (cart += ${ctx.loopItem}) and remove it there.`,
|
|
249
|
+
{ code: "WD309", file: ctx.file }
|
|
212
250
|
);
|
|
213
251
|
}
|
|
214
252
|
if (resolveStateKey(remove[1], ctx) !== ctx.loopKey) {
|
|
215
|
-
throw
|
|
216
|
-
`Button action "${raw}" must target the :state list being looped (@loop ${remove[1]} into ${ctx.loopItem}) in ${ctx.file}
|
|
253
|
+
throw wdError(
|
|
254
|
+
`Button action "${raw}" must target the :state list being looped (@loop ${remove[1]} into ${ctx.loopItem}) in ${ctx.file}.`,
|
|
255
|
+
{ code: "WD310", file: ctx.file }
|
|
217
256
|
);
|
|
218
257
|
}
|
|
219
258
|
return { op: "remove", target: ctx.loopKey };
|
|
@@ -249,7 +288,12 @@ function parseSingleAction(expression, raw, ctx) {
|
|
|
249
288
|
target: resolveTarget(assign[1]),
|
|
250
289
|
value: parseActionLiteral(assign[2], ctx)
|
|
251
290
|
};
|
|
252
|
-
throw
|
|
291
|
+
throw wdError(`Unsupported button action "${raw}" in ${ctx.file}. ${ACTION_USE}`, {
|
|
292
|
+
code: "WD311",
|
|
293
|
+
file: ctx.file,
|
|
294
|
+
hint: ACTION_USE.slice("Use: ".length),
|
|
295
|
+
example: ACTION_EXAMPLE
|
|
296
|
+
});
|
|
253
297
|
}
|
|
254
298
|
|
|
255
299
|
/**
|
|
@@ -264,15 +308,22 @@ function parseMergeOperand(raw, ctx) {
|
|
|
264
308
|
if (/^[A-Za-z_$][\w$]*$/.test(value)) {
|
|
265
309
|
const key = resolveStateKey(value, ctx);
|
|
266
310
|
if (!key)
|
|
267
|
-
throw
|
|
268
|
-
`Button action merge targets unknown state "${value}" in ${ctx.file}. Declare it first with :state ${value} =
|
|
311
|
+
throw wdError(
|
|
312
|
+
`Button action merge targets unknown state "${value}" in ${ctx.file}. Declare it first with :state ${value} = ...`,
|
|
313
|
+
{ code: "WD312", file: ctx.file }
|
|
269
314
|
);
|
|
270
315
|
return key;
|
|
271
316
|
}
|
|
272
317
|
const parsed = parseActionLiteral(value, ctx);
|
|
273
318
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
274
|
-
throw
|
|
275
|
-
`Unsupported merge operand "${raw}" in ${ctx.file}. Use: obj merge other (a state key or an inline {…} object)
|
|
319
|
+
throw wdError(
|
|
320
|
+
`Unsupported merge operand "${raw}" in ${ctx.file}. Use: obj merge other (a state key or an inline {…} object) — e.g. settings merge patch`,
|
|
321
|
+
{
|
|
322
|
+
code: "WD313",
|
|
323
|
+
file: ctx.file,
|
|
324
|
+
hint: "obj merge other (a state key or an inline {…} object) — e.g. settings merge patch",
|
|
325
|
+
example: "settings merge patch"
|
|
326
|
+
}
|
|
276
327
|
);
|
|
277
328
|
}
|
|
278
329
|
|
|
@@ -292,12 +343,14 @@ function parseActionLiteral(raw, ctx) {
|
|
|
292
343
|
try {
|
|
293
344
|
return JSON.parse(value);
|
|
294
345
|
} catch {
|
|
295
|
-
throw
|
|
296
|
-
`Unsupported action literal "${raw}" in ${ctx.file}. Use: a quoted string, number, boolean, null, or valid JSON
|
|
346
|
+
throw wdError(
|
|
347
|
+
`Unsupported action literal "${raw}" in ${ctx.file}. Use: a quoted string, number, boolean, null, or valid JSON.`,
|
|
348
|
+
{ code: "WD314", file: ctx.file }
|
|
297
349
|
);
|
|
298
350
|
}
|
|
299
351
|
}
|
|
300
|
-
throw
|
|
301
|
-
`Unsupported action literal "${raw}" in ${ctx.file}. Use: a quoted string, number, boolean, null, or valid JSON
|
|
352
|
+
throw wdError(
|
|
353
|
+
`Unsupported action literal "${raw}" in ${ctx.file}. Use: a quoted string, number, boolean, null, or valid JSON.`,
|
|
354
|
+
{ code: "WD314", file: ctx.file }
|
|
302
355
|
);
|
|
303
356
|
}
|
package/src/compiler/body.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// multi-line directive bodies with nesting + fence awareness.
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
|
-
import { at } from "./context.js";
|
|
10
|
+
import { at, lineOf, wdError } from "./context.js";
|
|
11
11
|
import {
|
|
12
12
|
handleBind,
|
|
13
13
|
handleButton,
|
|
@@ -92,7 +92,10 @@ export function compileBody(lines, ctx) {
|
|
|
92
92
|
if (container) {
|
|
93
93
|
flush();
|
|
94
94
|
if (!container[1].trim())
|
|
95
|
-
throw
|
|
95
|
+
throw wdError(`Stray ::: close with no open container in ${ctx.file}`, {
|
|
96
|
+
code: "WD007",
|
|
97
|
+
file: ctx.file
|
|
98
|
+
});
|
|
96
99
|
const block = scanContainer(lines, i, ctx);
|
|
97
100
|
out.push(handleContainer(container[1].trim(), block.body, ctx, i));
|
|
98
101
|
i = block.end;
|
|
@@ -235,17 +238,22 @@ export function compileBody(lines, ctx) {
|
|
|
235
238
|
continue;
|
|
236
239
|
}
|
|
237
240
|
if (/^@repeat\b/.test(line)) {
|
|
238
|
-
throw
|
|
239
|
-
`@repeat was replaced by @loop in ${ctx.file}. Use: @loop /data.json into item ... @endloop
|
|
241
|
+
throw wdError(
|
|
242
|
+
`@repeat was replaced by @loop in ${ctx.file}. Use: @loop /data.json into item ... @endloop`,
|
|
243
|
+
{ code: "WD008", file: ctx.file, hint: "@loop /data.json into item ... @endloop" }
|
|
240
244
|
);
|
|
241
245
|
}
|
|
242
246
|
if (/^:for\b/.test(line)) {
|
|
243
|
-
throw
|
|
244
|
-
`:for was replaced by @loop in ${ctx.file}. Use: @loop items into item ... @endloop
|
|
247
|
+
throw wdError(
|
|
248
|
+
`:for was replaced by @loop in ${ctx.file}. Use: @loop items into item ... @endloop`,
|
|
249
|
+
{ code: "WD009", file: ctx.file, hint: "@loop items into item ... @endloop" }
|
|
245
250
|
);
|
|
246
251
|
}
|
|
247
252
|
if (/^(@endloop|:endif|:endfor|:endform|:else)\s*$/.test(line)) {
|
|
248
|
-
throw
|
|
253
|
+
throw wdError(`Stray "${line.trim()}" with no matching opener in ${ctx.file}`, {
|
|
254
|
+
code: "WD010",
|
|
255
|
+
file: ctx.file
|
|
256
|
+
});
|
|
249
257
|
}
|
|
250
258
|
warnUnknownDirective(line, ctx);
|
|
251
259
|
prose.push(line);
|
|
@@ -371,7 +379,11 @@ function scanBlock(lines, start, openRe, endToken, ctx) {
|
|
|
371
379
|
}
|
|
372
380
|
body.push(line);
|
|
373
381
|
}
|
|
374
|
-
throw
|
|
382
|
+
throw wdError(`Missing ${endToken} for "${lines[start]}" in ${at(ctx, start)}`, {
|
|
383
|
+
code: "WD011",
|
|
384
|
+
file: ctx.file,
|
|
385
|
+
line: lineOf(ctx, start)
|
|
386
|
+
});
|
|
375
387
|
}
|
|
376
388
|
|
|
377
389
|
/**
|
|
@@ -457,7 +469,11 @@ function scanContainer(lines, start, ctx) {
|
|
|
457
469
|
}
|
|
458
470
|
body.push(line);
|
|
459
471
|
}
|
|
460
|
-
throw
|
|
472
|
+
throw wdError(`Missing closing ::: for "${lines[start]}" in ${at(ctx, start)}`, {
|
|
473
|
+
code: "WD012",
|
|
474
|
+
file: ctx.file,
|
|
475
|
+
line: lineOf(ctx, start)
|
|
476
|
+
});
|
|
461
477
|
}
|
|
462
478
|
|
|
463
479
|
/**
|
|
@@ -518,8 +534,9 @@ function scanConditional(lines, start, ctx) {
|
|
|
518
534
|
const elseIf = t.match(/^:else if\s+(.+?)\s*$/);
|
|
519
535
|
if (elseIf) {
|
|
520
536
|
if (mode === "else")
|
|
521
|
-
throw
|
|
522
|
-
`":else if" after ":else" in ${ctx.file}. ":else" must be the last branch — order the "else if" conditions before the bare ":else"
|
|
537
|
+
throw wdError(
|
|
538
|
+
`":else if" after ":else" in ${ctx.file}. ":else" must be the last branch — order the "else if" conditions before the bare ":else".`,
|
|
539
|
+
{ code: "WD013", file: ctx.file }
|
|
523
540
|
);
|
|
524
541
|
falsy.push(`:if ${elseIf[1]}`); // desugar the chain tail into a nested :if
|
|
525
542
|
falsyStart = i;
|
|
@@ -529,8 +546,9 @@ function scanConditional(lines, start, ctx) {
|
|
|
529
546
|
}
|
|
530
547
|
if (t === ":else") {
|
|
531
548
|
if (mode === "else")
|
|
532
|
-
throw
|
|
533
|
-
`Duplicate ":else" in ${ctx.file}. A conditional may have only one bare ":else"
|
|
549
|
+
throw wdError(
|
|
550
|
+
`Duplicate ":else" in ${ctx.file}. A conditional may have only one bare ":else".`,
|
|
551
|
+
{ code: "WD014", file: ctx.file }
|
|
534
552
|
);
|
|
535
553
|
falsyStart = i + 1;
|
|
536
554
|
current = falsy;
|
|
@@ -540,5 +558,9 @@ function scanConditional(lines, start, ctx) {
|
|
|
540
558
|
}
|
|
541
559
|
current.push(line);
|
|
542
560
|
}
|
|
543
|
-
throw
|
|
561
|
+
throw wdError(`Missing :endif for "${lines[start]}" in ${at(ctx, start)}`, {
|
|
562
|
+
code: "WD015",
|
|
563
|
+
file: ctx.file,
|
|
564
|
+
line: lineOf(ctx, start)
|
|
565
|
+
});
|
|
544
566
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { firstParagraph } from "../feeds.js";
|
|
20
|
+
import { wdError } from "./context.js";
|
|
20
21
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
21
22
|
import { fsReader } from "./fs-reader.js";
|
|
22
23
|
|
|
@@ -197,8 +198,9 @@ export function parseSchema(raw, file) {
|
|
|
197
198
|
if (!trimmed || trimmed.startsWith("#")) continue; // blank line or comment
|
|
198
199
|
const match = trimmed.match(/^([A-Za-z0-9_-]+):\s*(.+)$/);
|
|
199
200
|
if (!match) {
|
|
200
|
-
throw
|
|
201
|
-
`Malformed schema line in ${file}:${i + 1}: "${trimmed}". Use: field: type (e.g. \`title: string\`, \`tags: string[]\`, \`featured: boolean?\`)
|
|
201
|
+
throw wdError(
|
|
202
|
+
`Malformed schema line in ${file}:${i + 1}: "${trimmed}". Use: field: type (e.g. \`title: string\`, \`tags: string[]\`, \`featured: boolean?\`).`,
|
|
203
|
+
{ code: "WD120", file, line: i + 1 }
|
|
202
204
|
);
|
|
203
205
|
}
|
|
204
206
|
fields.push(parseSchemaField(match[1], match[2].trim(), file, i + 1));
|
|
@@ -219,8 +221,9 @@ function parseSchemaField(name, rawType, file, lineNo) {
|
|
|
219
221
|
const optional = rawType.endsWith("?");
|
|
220
222
|
const type = optional ? rawType.slice(0, -1).trim() : rawType;
|
|
221
223
|
if (!SCHEMA_TYPES.has(type)) {
|
|
222
|
-
throw
|
|
223
|
-
`Unknown schema type "${rawType}" for field "${name}" in ${file}:${lineNo}. Use one of: ${[...SCHEMA_TYPES].join(", ")} (append "?" to make a field optional)
|
|
224
|
+
throw wdError(
|
|
225
|
+
`Unknown schema type "${rawType}" for field "${name}" in ${file}:${lineNo}. Use one of: ${[...SCHEMA_TYPES].join(", ")} (append "?" to make a field optional).`,
|
|
226
|
+
{ code: "WD121", file, line: lineNo }
|
|
224
227
|
);
|
|
225
228
|
}
|
|
226
229
|
return { name, type, optional };
|
|
@@ -249,8 +252,9 @@ function validateRows(rows, schema, name) {
|
|
|
249
252
|
for (const field of schema) validateField(row, field, where);
|
|
250
253
|
for (const key of Object.keys(row)) {
|
|
251
254
|
if (allowed.has(key)) continue;
|
|
252
|
-
throw
|
|
253
|
-
`Unknown frontmatter field "${key}" in ${where}: it is not declared in _schema.wd. Add \`${key}: <type>\` to the schema, or remove the field
|
|
255
|
+
throw wdError(
|
|
256
|
+
`Unknown frontmatter field "${key}" in ${where}: it is not declared in _schema.wd. Add \`${key}: <type>\` to the schema, or remove the field.`,
|
|
257
|
+
{ code: "WD122" }
|
|
254
258
|
);
|
|
255
259
|
}
|
|
256
260
|
}
|
|
@@ -267,14 +271,16 @@ function validateField(row, field, where) {
|
|
|
267
271
|
const present = field.name in row && row[field.name] !== null && row[field.name] !== "";
|
|
268
272
|
if (!present) {
|
|
269
273
|
if (field.optional) return;
|
|
270
|
-
throw
|
|
271
|
-
`Missing required field "${field.name}" (${field.type}) in ${where}. Add \`${field.name}: …\` to the entry's frontmatter, or mark it optional with "${field.name}: ${field.type}?" in _schema.wd
|
|
274
|
+
throw wdError(
|
|
275
|
+
`Missing required field "${field.name}" (${field.type}) in ${where}. Add \`${field.name}: …\` to the entry's frontmatter, or mark it optional with "${field.name}: ${field.type}?" in _schema.wd.`,
|
|
276
|
+
{ code: "WD123" }
|
|
272
277
|
);
|
|
273
278
|
}
|
|
274
279
|
const value = row[field.name];
|
|
275
280
|
if (!matchesType(value, field.type)) {
|
|
276
|
-
throw
|
|
277
|
-
`Field "${field.name}" in ${where} should be ${field.type} but is "${describe(value)}". Fix the entry's frontmatter or the schema
|
|
281
|
+
throw wdError(
|
|
282
|
+
`Field "${field.name}" in ${where} should be ${field.type} but is "${describe(value)}". Fix the entry's frontmatter or the schema.`,
|
|
283
|
+
{ code: "WD124" }
|
|
278
284
|
);
|
|
279
285
|
}
|
|
280
286
|
}
|
package/src/compiler/context.js
CHANGED
|
@@ -152,6 +152,27 @@
|
|
|
152
152
|
* @property {Set<string>} collectionsUsed Collection names this compile looped.
|
|
153
153
|
*/
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Structured, machine-readable companion to a compile error's string message.
|
|
157
|
+
* The string `message` contract is unchanged; `wd` gives an AI edit-loop (or an
|
|
158
|
+
* editor) the same facts without re-parsing the prose: which code, which
|
|
159
|
+
* file/line, the corrective `Use:` template, and one concrete compilable
|
|
160
|
+
* example line.
|
|
161
|
+
* @typedef {object} WdErrorInfo
|
|
162
|
+
* @property {string} code Stable error code (`WD` + three digits, e.g. `WD201`).
|
|
163
|
+
* Every code is registered in `src/errors.js` with a title, cause, and fix,
|
|
164
|
+
* and documented in `docs/errors.md`. {@link wdError} prefixes the string
|
|
165
|
+
* message with `[<code>] `, so the code is greppable from the message too.
|
|
166
|
+
* @property {string} [file] Absolute path of the source file the error is in,
|
|
167
|
+
* when the throwing layer knows one (`compileSkin` is a pure string→string
|
|
168
|
+
* pass and a collection-row check names the entry route instead).
|
|
169
|
+
* @property {number} [line] 1-based file line the error points at, when known
|
|
170
|
+
* (some errors — e.g. a bad `where` operand — are file-scoped only, matching
|
|
171
|
+
* the string message, which likewise omits the line there).
|
|
172
|
+
* @property {string} [hint] The corrective `Use:` template (without the `Use: ` prefix).
|
|
173
|
+
* @property {string} [example] One concrete, compilable directive line.
|
|
174
|
+
*/
|
|
175
|
+
|
|
155
176
|
/**
|
|
156
177
|
* A compiled `@loop … where` predicate.
|
|
157
178
|
* @typedef {object} Predicate
|
|
@@ -193,6 +214,23 @@
|
|
|
193
214
|
*/
|
|
194
215
|
|
|
195
216
|
// Page-include extensions are the only two file types `@include` will pull in.
|
|
217
|
+
/**
|
|
218
|
+
* Normalize CRLF/CR line endings to LF.
|
|
219
|
+
*
|
|
220
|
+
* Every line-structured parser in the project (frontmatter delimiters, the `.wd`
|
|
221
|
+
* directive dispatcher, `.skin` indentation) tests LF-shaped text. Source files
|
|
222
|
+
* authored on Windows — or checked out anywhere with git's `core.autocrlf`, the
|
|
223
|
+
* default on Windows — arrive CRLF-terminated, and an un-normalized `\r` turns
|
|
224
|
+
* `---` into `---\r` and silently defeats those tests. Applied at the reader
|
|
225
|
+
* boundary and again at the two entry points that accept raw strings from
|
|
226
|
+
* callers that bypass the reader; it is idempotent, so doubling up is free.
|
|
227
|
+
* @param {string} text
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
export function normalizeNewlines(text) {
|
|
231
|
+
return text.includes("\r") ? text.replace(/\r\n?/g, "\n") : text;
|
|
232
|
+
}
|
|
233
|
+
|
|
196
234
|
export const pageIncludeExtensions = [".md", ".wd"];
|
|
197
235
|
|
|
198
236
|
// Reserved per-row meta variables, mapped to their runtime marker token. Shared
|
|
@@ -257,7 +295,38 @@ export function createScope(parent, vars = {}) {
|
|
|
257
295
|
* @returns {string}
|
|
258
296
|
*/
|
|
259
297
|
export function at(ctx, index) {
|
|
260
|
-
return `${ctx.file}:${
|
|
298
|
+
return `${ctx.file}:${lineOf(ctx, index)}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The 1-based true file line for a body-slice `index` — the numeric half of
|
|
303
|
+
* {@link at}, exposed so {@link wdError} can populate the structured `wd.line`.
|
|
304
|
+
* @param {Pick<Ctx, "bodyLine" | "lineOffset">} ctx
|
|
305
|
+
* @param {number} index 0-based line index into the current body slice.
|
|
306
|
+
* @returns {number}
|
|
307
|
+
*/
|
|
308
|
+
export function lineOf(ctx, index) {
|
|
309
|
+
return index + 1 + (ctx.bodyLine ?? 0) + (ctx.lineOffset ?? 0);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Build a compile `Error` carrying a structured {@link WdErrorInfo} on `err.wd`
|
|
314
|
+
* — so an AI edit-loop (or an editor) gets code/file/line/hint/example without
|
|
315
|
+
* re-parsing the prose. The string message is the caller's text prefixed with
|
|
316
|
+
* the stable `[WDxxx]` code, so a user can search the code straight out of the
|
|
317
|
+
* terminal; everything after the prefix (the `file:line`, the corrective `Use:`
|
|
318
|
+
* template, the concrete `e.g.` example) is unchanged.
|
|
319
|
+
*
|
|
320
|
+
* Every author-facing compile error is built here, so no error can ship without
|
|
321
|
+
* a code. See `src/errors.js` for the registry and the numbering scheme.
|
|
322
|
+
* @param {string} message The user-facing error text, without the code prefix.
|
|
323
|
+
* @param {WdErrorInfo} wd The structured mirror (its `code` becomes the prefix).
|
|
324
|
+
* @returns {Error & { wd: WdErrorInfo }}
|
|
325
|
+
*/
|
|
326
|
+
export function wdError(message, wd) {
|
|
327
|
+
const err = /** @type {Error & { wd: WdErrorInfo }} */ (new Error(`[${wd.code}] ${message}`));
|
|
328
|
+
err.wd = wd;
|
|
329
|
+
return err;
|
|
261
330
|
}
|
|
262
331
|
|
|
263
332
|
/**
|
package/src/compiler/fetch.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// while keeping the message text and `Use:` hints intact.
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
|
-
import { at } from "./context.js";
|
|
10
|
+
import { at, lineOf, wdError } from "./context.js";
|
|
11
11
|
import {
|
|
12
12
|
escapeHtml,
|
|
13
13
|
resolveStateKey,
|
|
@@ -21,8 +21,10 @@ import { declareState } from "./state.js";
|
|
|
21
21
|
* @typedef {import("./context.js").Ctx} Ctx
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// A concrete, compilable :fetch line for the hint tail + the directive catalog.
|
|
25
|
+
export const FETCH_EXAMPLE = ':fetch todos from "/api/todos.json" when=visible';
|
|
26
|
+
|
|
27
|
+
const FETCH_USE = `Use: :fetch name from "url" [method=…] [timeout=ms] [retry=N] [when=visible] [headers=key] [body=key] [refresh=url] — e.g. ${FETCH_EXAMPLE}`;
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Parse a keyword-arg `:fetch` directive into a lifecycle-aware marker.
|
|
@@ -36,17 +38,33 @@ const FETCH_USE =
|
|
|
36
38
|
*/
|
|
37
39
|
export function handleFetch(line, ctx, index) {
|
|
38
40
|
const head = line.match(/^:fetch\s+([A-Za-z_$][\w$]*)\s+from\s+("[^"]+"|\S+)\s*(.*)$/);
|
|
39
|
-
if (!head)
|
|
41
|
+
if (!head)
|
|
42
|
+
throw wdError(`Malformed :fetch in ${at(ctx, index)}: ${line}. ${FETCH_USE}`, {
|
|
43
|
+
code: "WD501",
|
|
44
|
+
file: ctx.file,
|
|
45
|
+
line: lineOf(ctx, index),
|
|
46
|
+
hint: FETCH_USE.slice("Use: ".length),
|
|
47
|
+
example: FETCH_EXAMPLE
|
|
48
|
+
});
|
|
40
49
|
const name = head[1];
|
|
41
50
|
const url = validateFetchUrl(stripQuotes(head[2]), ctx);
|
|
42
51
|
/** @type {Record<string, string>} */
|
|
43
52
|
const opts = {};
|
|
44
53
|
for (const part of head[3].trim().split(/\s+/).filter(Boolean)) {
|
|
45
54
|
const kv = part.match(/^([A-Za-z]+)=(.+)$/);
|
|
46
|
-
if (!kv)
|
|
55
|
+
if (!kv)
|
|
56
|
+
throw wdError(`Unknown :fetch option "${part}" in ${at(ctx, index)}. ${FETCH_USE}`, {
|
|
57
|
+
code: "WD502",
|
|
58
|
+
file: ctx.file,
|
|
59
|
+
line: lineOf(ctx, index)
|
|
60
|
+
});
|
|
47
61
|
const optName = kv[1];
|
|
48
62
|
if (!["method", "when", "timeout", "retry", "headers", "body", "refresh"].includes(optName)) {
|
|
49
|
-
throw
|
|
63
|
+
throw wdError(`Unknown :fetch option "${optName}" in ${at(ctx, index)}. ${FETCH_USE}`, {
|
|
64
|
+
code: "WD502",
|
|
65
|
+
file: ctx.file,
|
|
66
|
+
line: lineOf(ctx, index)
|
|
67
|
+
});
|
|
50
68
|
}
|
|
51
69
|
opts[optName] = stripQuotes(kv[2]);
|
|
52
70
|
}
|
|
@@ -55,17 +73,27 @@ export function handleFetch(line, ctx, index) {
|
|
|
55
73
|
opts.method &&
|
|
56
74
|
!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(opts.method.toUpperCase())
|
|
57
75
|
) {
|
|
58
|
-
throw
|
|
59
|
-
`:fetch method "${opts.method}" is not allowed in ${at(ctx, index)}. ${FETCH_USE}
|
|
76
|
+
throw wdError(
|
|
77
|
+
`:fetch method "${opts.method}" is not allowed in ${at(ctx, index)}. ${FETCH_USE}`,
|
|
78
|
+
{
|
|
79
|
+
code: "WD503",
|
|
80
|
+
file: ctx.file,
|
|
81
|
+
line: lineOf(ctx, index)
|
|
82
|
+
}
|
|
60
83
|
);
|
|
61
84
|
}
|
|
62
85
|
if (opts.when && !["load", "visible"].includes(opts.when)) {
|
|
63
|
-
throw
|
|
86
|
+
throw wdError(`:fetch when "${opts.when}" is not allowed in ${at(ctx, index)}. ${FETCH_USE}`, {
|
|
87
|
+
code: "WD504",
|
|
88
|
+
file: ctx.file,
|
|
89
|
+
line: lineOf(ctx, index)
|
|
90
|
+
});
|
|
64
91
|
}
|
|
65
92
|
for (const n of ["timeout", "retry"]) {
|
|
66
93
|
if (opts[n] !== undefined && !/^\d+$/.test(opts[n])) {
|
|
67
|
-
throw
|
|
68
|
-
`:fetch ${n} must be a non-negative integer in ${at(ctx, index)}. ${FETCH_USE}
|
|
94
|
+
throw wdError(
|
|
95
|
+
`:fetch ${n} must be a non-negative integer in ${at(ctx, index)}. ${FETCH_USE}`,
|
|
96
|
+
{ code: "WD505", file: ctx.file, line: lineOf(ctx, index) }
|
|
69
97
|
);
|
|
70
98
|
}
|
|
71
99
|
}
|
|
@@ -73,8 +101,9 @@ export function handleFetch(line, ctx, index) {
|
|
|
73
101
|
// Layer 2: a 401 triggers a token-refresh POST to this URL, then one retry.
|
|
74
102
|
// The new token is written back into the `headers=` state, so it is required.
|
|
75
103
|
if (!opts.headers) {
|
|
76
|
-
throw
|
|
77
|
-
`:fetch refresh= needs headers= (the state key holding the token to renew) in ${at(ctx, index)}. ${FETCH_USE}
|
|
104
|
+
throw wdError(
|
|
105
|
+
`:fetch refresh= needs headers= (the state key holding the token to renew) in ${at(ctx, index)}. ${FETCH_USE}`,
|
|
106
|
+
{ code: "WD506", file: ctx.file, line: lineOf(ctx, index) }
|
|
78
107
|
);
|
|
79
108
|
}
|
|
80
109
|
opts.refresh = validateFetchUrl(opts.refresh, ctx, ":fetch refresh");
|
|
@@ -139,20 +168,23 @@ export function validateFetchUrl(url, ctx, what = ":fetch") {
|
|
|
139
168
|
const value = url.trim();
|
|
140
169
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — rejects control characters in URLs/hrefs.
|
|
141
170
|
if (value !== url || value === "" || /[\u0000-\u001F\u007F]/.test(value)) {
|
|
142
|
-
throw
|
|
143
|
-
`Unsafe ${what} URL "${escapeHtml(url)}" in ${ctx.file}. Use a relative path (/, ./, ../), an http(s):// URL, or a { state } interpolation
|
|
171
|
+
throw wdError(
|
|
172
|
+
`Unsafe ${what} URL "${escapeHtml(url)}" in ${ctx.file}. Use a relative path (/, ./, ../), an http(s):// URL, or a { state } interpolation.`,
|
|
173
|
+
{ code: "WD507", file: ctx.file }
|
|
144
174
|
);
|
|
145
175
|
}
|
|
146
176
|
if (value.startsWith("//")) {
|
|
147
|
-
throw
|
|
148
|
-
`Unsafe ${what} URL "${escapeHtml(url)}" in ${ctx.file}. Protocol-relative URLs are not allowed; use http:// or https:// explicitly
|
|
177
|
+
throw wdError(
|
|
178
|
+
`Unsafe ${what} URL "${escapeHtml(url)}" in ${ctx.file}. Protocol-relative URLs are not allowed; use http:// or https:// explicitly.`,
|
|
179
|
+
{ code: "WD508", file: ctx.file }
|
|
149
180
|
);
|
|
150
181
|
}
|
|
151
182
|
if (value.startsWith("{")) return value; // interpolation-first; the runtime percent-encodes state values
|
|
152
183
|
const scheme = value.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
|
|
153
184
|
if (scheme && !["http", "https"].includes(scheme[1].toLowerCase())) {
|
|
154
|
-
throw
|
|
155
|
-
`Unsafe ${what} URL "${escapeHtml(url)}" in ${ctx.file}. The "${scheme[1]}:" scheme is not allowed; use http://, https://, or a relative path
|
|
185
|
+
throw wdError(
|
|
186
|
+
`Unsafe ${what} URL "${escapeHtml(url)}" in ${ctx.file}. The "${scheme[1]}:" scheme is not allowed; use http://, https://, or a relative path.`,
|
|
187
|
+
{ code: "WD509", file: ctx.file }
|
|
156
188
|
);
|
|
157
189
|
}
|
|
158
190
|
return value;
|
package/src/compiler/format.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// reads :state/:store emits `data-wd-fmt` and re-formats in the runtime.
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
|
|
14
|
+
import { wdError } from "./context.js";
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
17
|
* @typedef {import("./context.js").Ctx} Ctx
|
|
16
18
|
* @typedef {{ name: string, args: Array<string | number | boolean | null> }} Stage
|
|
@@ -221,8 +223,9 @@ export function validatePipes(inner, ctx) {
|
|
|
221
223
|
const parsed = parsePipes(inner);
|
|
222
224
|
for (const stage of parsed.stages) {
|
|
223
225
|
if (!Object.hasOwn(FORMATTERS, stage.name)) {
|
|
224
|
-
throw
|
|
225
|
-
`Unknown formatter "${stage.name}" in { ${inner} } (${ctx.file}). Available: ${FORMATTER_NAMES.join(", ")}
|
|
226
|
+
throw wdError(
|
|
227
|
+
`Unknown formatter "${stage.name}" in { ${inner} } (${ctx.file}). Available: ${FORMATTER_NAMES.join(", ")}.`,
|
|
228
|
+
{ code: "WD006", file: ctx.file }
|
|
226
229
|
);
|
|
227
230
|
}
|
|
228
231
|
}
|