@zvndev/darkmown 2.1.0 → 2.3.0
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 +661 -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 +56 -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/statics.d.ts +2 -1
package/src/compiler/forms.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// while keeping the message text and `Use:` hints intact.
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
|
|
12
|
-
import { at, nestedCtx } from "./context.js";
|
|
12
|
+
import { at, lineOf, nestedCtx, wdError } from "./context.js";
|
|
13
13
|
import { validateFetchUrl } from "./fetch.js";
|
|
14
14
|
import {
|
|
15
15
|
escapeHtml,
|
|
@@ -25,6 +25,20 @@ import { declareErrorState, declareState } from "./state.js";
|
|
|
25
25
|
* @typedef {import("./context.js").Ctx} Ctx
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
// Concrete, compilable one-liners for the form-family hint tails + the catalog.
|
|
29
|
+
// (Field openers like :select/:checkbox expect following `- Label` lines; the
|
|
30
|
+
// example is the opener, which the catalog/grammar pair with option lines.)
|
|
31
|
+
export const FORM_EXAMPLE = ":form into contact";
|
|
32
|
+
export const INPUT_EXAMPLE = ":input email type=email required";
|
|
33
|
+
export const TEXTAREA_EXAMPLE = ':textarea message placeholder="Your message" rows=4';
|
|
34
|
+
export const SELECT_EXAMPLE = ":select topic";
|
|
35
|
+
export const CHECKBOX_EXAMPLE = ":checkbox toppings";
|
|
36
|
+
export const RADIO_EXAMPLE = ":radio size";
|
|
37
|
+
export const SUBMIT_EXAMPLE = ':submit "Send"';
|
|
38
|
+
export const BIND_EXAMPLE = ':bind query placeholder="Search"';
|
|
39
|
+
export const SLIDER_EXAMPLE = ":slider volume = 50 min=0 max=100 step=1";
|
|
40
|
+
const INPUT_USE = `Use: :input name [type=…] [placeholder="…"] [required] — e.g. ${INPUT_EXAMPLE}`;
|
|
41
|
+
|
|
28
42
|
/**
|
|
29
43
|
* @param {string} line
|
|
30
44
|
* @param {string[]} bodyLines
|
|
@@ -43,9 +57,14 @@ export function handleForm(line, bodyLines, ctx, index) {
|
|
|
43
57
|
.replace(/(?:^|\s)into\s+[A-Za-z_$][\w$]*/, "")
|
|
44
58
|
.trim();
|
|
45
59
|
if ((!rawAction && !into) || leftover) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
const hint = `':form into name' (client state), ':form action="/url"' (native post), or both (fetch round-trip into state) — e.g. ${FORM_EXAMPLE}`;
|
|
61
|
+
throw wdError(`Malformed :form in ${at(ctx, index)}: ${line}. Use ${hint}`, {
|
|
62
|
+
code: "WD401",
|
|
63
|
+
file: ctx.file,
|
|
64
|
+
line: lineOf(ctx, index),
|
|
65
|
+
hint,
|
|
66
|
+
example: FORM_EXAMPLE
|
|
67
|
+
});
|
|
49
68
|
}
|
|
50
69
|
const action = rawAction ? validateFetchUrl(rawAction, ctx, ":form action") : undefined;
|
|
51
70
|
const inner = ctx.compileBody(bodyLines, nestedCtx(ctx, index + 1)).trim();
|
|
@@ -68,7 +87,14 @@ export function handleForm(line, bodyLines, ctx, index) {
|
|
|
68
87
|
*/
|
|
69
88
|
export function handleInput(line, ctx, index) {
|
|
70
89
|
const match = line.match(/^:input\s+([A-Za-z_][\w-]*)\s*(.*)$/);
|
|
71
|
-
if (!match)
|
|
90
|
+
if (!match)
|
|
91
|
+
throw wdError(`Malformed :input in ${at(ctx, index)}: ${line}. ${INPUT_USE}`, {
|
|
92
|
+
code: "WD402",
|
|
93
|
+
file: ctx.file,
|
|
94
|
+
line: lineOf(ctx, index),
|
|
95
|
+
hint: INPUT_USE.slice("Use: ".length),
|
|
96
|
+
example: INPUT_EXAMPLE
|
|
97
|
+
});
|
|
72
98
|
const attrs = [`name="${escapeHtml(match[1])}"`];
|
|
73
99
|
let type = "text";
|
|
74
100
|
let placeholder;
|
|
@@ -77,7 +103,11 @@ export function handleInput(line, ctx, index) {
|
|
|
77
103
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
78
104
|
if (token[3]) {
|
|
79
105
|
if (!["required", "autofocus", "disabled", "readonly"].includes(token[3])) {
|
|
80
|
-
throw
|
|
106
|
+
throw wdError(`Unknown :input flag "${token[3]}" in ${at(ctx, index)}`, {
|
|
107
|
+
code: "WD403",
|
|
108
|
+
file: ctx.file,
|
|
109
|
+
line: lineOf(ctx, index)
|
|
110
|
+
});
|
|
81
111
|
}
|
|
82
112
|
attrs.push(token[3]);
|
|
83
113
|
continue;
|
|
@@ -100,7 +130,11 @@ export function handleInput(line, ctx, index) {
|
|
|
100
130
|
"aria-describedby"
|
|
101
131
|
].includes(token[1])
|
|
102
132
|
) {
|
|
103
|
-
throw
|
|
133
|
+
throw wdError(`Unknown :input attribute "${token[1]}" in ${at(ctx, index)}`, {
|
|
134
|
+
code: "WD404",
|
|
135
|
+
file: ctx.file,
|
|
136
|
+
line: lineOf(ctx, index)
|
|
137
|
+
});
|
|
104
138
|
}
|
|
105
139
|
if (token[1] === "placeholder") placeholder = value;
|
|
106
140
|
if (token[1] === "aria-label" || token[1] === "aria-describedby") hasAria = true;
|
|
@@ -127,13 +161,18 @@ export function handleInput(line, ctx, index) {
|
|
|
127
161
|
export function handleBind(line, ctx, index) {
|
|
128
162
|
const match = line.match(/^:bind\s+([A-Za-z_$][\w$]*)\s*(.*)$/);
|
|
129
163
|
if (!match)
|
|
130
|
-
throw
|
|
131
|
-
|
|
132
|
-
|
|
164
|
+
throw wdError(`Malformed :bind in ${at(ctx, index)}: ${line}. Use: ${BIND_EXAMPLE}`, {
|
|
165
|
+
code: "WD405",
|
|
166
|
+
file: ctx.file,
|
|
167
|
+
line: lineOf(ctx, index),
|
|
168
|
+
hint: BIND_EXAMPLE,
|
|
169
|
+
example: BIND_EXAMPLE
|
|
170
|
+
});
|
|
133
171
|
const key = resolveStateKey(match[1], ctx);
|
|
134
172
|
if (!key) {
|
|
135
|
-
throw
|
|
136
|
-
`:bind ${match[1]} in ${ctx.file} has no matching state. Declare it first: :state ${match[1]} = ""
|
|
173
|
+
throw wdError(
|
|
174
|
+
`:bind ${match[1]} in ${ctx.file} has no matching state. Declare it first: :state ${match[1]} = ""`,
|
|
175
|
+
{ code: "WD406", file: ctx.file }
|
|
137
176
|
);
|
|
138
177
|
}
|
|
139
178
|
ctx.comp.assets.runtime = true;
|
|
@@ -145,7 +184,11 @@ export function handleBind(line, ctx, index) {
|
|
|
145
184
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
146
185
|
if (token[3]) {
|
|
147
186
|
if (!["required", "autofocus"].includes(token[3]))
|
|
148
|
-
throw
|
|
187
|
+
throw wdError(`Unknown :bind flag "${token[3]}" in ${at(ctx, index)}`, {
|
|
188
|
+
code: "WD407",
|
|
189
|
+
file: ctx.file,
|
|
190
|
+
line: lineOf(ctx, index)
|
|
191
|
+
});
|
|
149
192
|
attrs.push(token[3]);
|
|
150
193
|
continue;
|
|
151
194
|
}
|
|
@@ -155,7 +198,11 @@ export function handleBind(line, ctx, index) {
|
|
|
155
198
|
continue;
|
|
156
199
|
}
|
|
157
200
|
if (!["placeholder", "autocomplete", "aria-label", "aria-describedby"].includes(token[1])) {
|
|
158
|
-
throw
|
|
201
|
+
throw wdError(`Unknown :bind attribute "${token[1]}" in ${at(ctx, index)}`, {
|
|
202
|
+
code: "WD408",
|
|
203
|
+
file: ctx.file,
|
|
204
|
+
line: lineOf(ctx, index)
|
|
205
|
+
});
|
|
159
206
|
}
|
|
160
207
|
if (token[1] === "placeholder") placeholder = value;
|
|
161
208
|
if (token[1] === "aria-label" || token[1] === "aria-describedby") hasAria = true;
|
|
@@ -187,9 +234,13 @@ export function handleBind(line, ctx, index) {
|
|
|
187
234
|
export function handleSlider(line, ctx, index) {
|
|
188
235
|
const head = line.match(/^:slider\s+([A-Za-z_$][\w$]*)\s*(.*)$/);
|
|
189
236
|
if (!head)
|
|
190
|
-
throw
|
|
191
|
-
|
|
192
|
-
|
|
237
|
+
throw wdError(`Malformed :slider in ${at(ctx, index)}: ${line}. Use: ${SLIDER_EXAMPLE}`, {
|
|
238
|
+
code: "WD409",
|
|
239
|
+
file: ctx.file,
|
|
240
|
+
line: lineOf(ctx, index),
|
|
241
|
+
hint: SLIDER_EXAMPLE,
|
|
242
|
+
example: SLIDER_EXAMPLE
|
|
243
|
+
});
|
|
193
244
|
const name = head[1];
|
|
194
245
|
let rest = head[2].trim();
|
|
195
246
|
|
|
@@ -198,8 +249,9 @@ export function handleSlider(line, ctx, index) {
|
|
|
198
249
|
if (rest.startsWith("=")) {
|
|
199
250
|
const valueMatch = rest.match(/^=\s*(\S+)\s*(.*)$/);
|
|
200
251
|
if (!valueMatch)
|
|
201
|
-
throw
|
|
202
|
-
`Malformed :slider initial value in ${at(ctx, index)}: ${line}. Use: :slider ${name} = 50
|
|
252
|
+
throw wdError(
|
|
253
|
+
`Malformed :slider initial value in ${at(ctx, index)}: ${line}. Use: :slider ${name} = 50`,
|
|
254
|
+
{ code: "WD410", file: ctx.file, line: lineOf(ctx, index) }
|
|
203
255
|
);
|
|
204
256
|
initialRaw = valueMatch[1];
|
|
205
257
|
rest = valueMatch[2].trim();
|
|
@@ -221,7 +273,12 @@ export function handleSlider(line, ctx, index) {
|
|
|
221
273
|
else if (token[1] === "max") max = value;
|
|
222
274
|
else if (token[1] === "step") step = value;
|
|
223
275
|
else if (token[1] === "aria-label") ariaLabel = value;
|
|
224
|
-
else
|
|
276
|
+
else
|
|
277
|
+
throw wdError(`Unknown :slider attribute "${token[1]}" in ${at(ctx, index)}`, {
|
|
278
|
+
code: "WD411",
|
|
279
|
+
file: ctx.file,
|
|
280
|
+
line: lineOf(ctx, index)
|
|
281
|
+
});
|
|
225
282
|
}
|
|
226
283
|
for (const [label, raw] of [
|
|
227
284
|
["min", min],
|
|
@@ -229,7 +286,11 @@ export function handleSlider(line, ctx, index) {
|
|
|
229
286
|
["step", step]
|
|
230
287
|
]) {
|
|
231
288
|
if (!/^-?\d+(?:\.\d+)?$/.test(raw))
|
|
232
|
-
throw
|
|
289
|
+
throw wdError(`:slider ${label} must be a number in ${at(ctx, index)}: ${raw}`, {
|
|
290
|
+
code: "WD412",
|
|
291
|
+
file: ctx.file,
|
|
292
|
+
line: lineOf(ctx, index)
|
|
293
|
+
});
|
|
233
294
|
}
|
|
234
295
|
|
|
235
296
|
ctx.comp.assets.runtime = true;
|
|
@@ -239,21 +300,24 @@ export function handleSlider(line, ctx, index) {
|
|
|
239
300
|
if (initialRaw !== undefined) {
|
|
240
301
|
const value = parseStateValue(initialRaw, at(ctx, index));
|
|
241
302
|
if (typeof value !== "number")
|
|
242
|
-
throw
|
|
243
|
-
`:slider ${name} initial value must be a number in ${at(ctx, index)}: ${initialRaw}
|
|
303
|
+
throw wdError(
|
|
304
|
+
`:slider ${name} initial value must be a number in ${at(ctx, index)}: ${initialRaw}`,
|
|
305
|
+
{ code: "WD413", file: ctx.file, line: lineOf(ctx, index) }
|
|
244
306
|
);
|
|
245
307
|
key = declareState(name, value, ctx);
|
|
246
308
|
const persistAttr = persist ? ` data-wd-persist="${key}"` : "";
|
|
247
309
|
seed = `<script type="application/json" data-wd-state${persistAttr}>${safeScriptJson({ [key]: value })}</script>`;
|
|
248
310
|
} else {
|
|
249
311
|
if (persist)
|
|
250
|
-
throw
|
|
251
|
-
`:slider persist only applies when declaring state inline (:slider ${name} = 0 … persist) in ${at(ctx, index)}
|
|
312
|
+
throw wdError(
|
|
313
|
+
`:slider persist only applies when declaring state inline (:slider ${name} = 0 … persist) in ${at(ctx, index)}`,
|
|
314
|
+
{ code: "WD414", file: ctx.file, line: lineOf(ctx, index) }
|
|
252
315
|
);
|
|
253
316
|
const resolved = resolveStateKey(name, ctx);
|
|
254
317
|
if (!resolved)
|
|
255
|
-
throw
|
|
256
|
-
`:slider ${name} in ${ctx.file} has no matching state. Declare it: :slider ${name} = 0 min=0 max=100
|
|
318
|
+
throw wdError(
|
|
319
|
+
`:slider ${name} in ${ctx.file} has no matching state. Declare it: :slider ${name} = 0 min=0 max=100`,
|
|
320
|
+
{ code: "WD415", file: ctx.file }
|
|
257
321
|
);
|
|
258
322
|
key = resolved;
|
|
259
323
|
}
|
|
@@ -274,7 +338,16 @@ export function handleSlider(line, ctx, index) {
|
|
|
274
338
|
export function handleSubmit(line, ctx, index) {
|
|
275
339
|
const match = line.match(/^:submit\s+"([^"]+)"\s*$/);
|
|
276
340
|
if (!match)
|
|
277
|
-
throw
|
|
341
|
+
throw wdError(
|
|
342
|
+
`Malformed :submit in ${at(ctx, index)}: ${line}. Use: :submit "Label" — e.g. ${SUBMIT_EXAMPLE}`,
|
|
343
|
+
{
|
|
344
|
+
code: "WD416",
|
|
345
|
+
file: ctx.file,
|
|
346
|
+
line: lineOf(ctx, index),
|
|
347
|
+
hint: `:submit "Label" — e.g. ${SUBMIT_EXAMPLE}`,
|
|
348
|
+
example: SUBMIT_EXAMPLE
|
|
349
|
+
}
|
|
350
|
+
);
|
|
278
351
|
return `<button type="submit">${escapeHtml(match[1])}</button>`;
|
|
279
352
|
}
|
|
280
353
|
|
|
@@ -290,10 +363,16 @@ export function handleSubmit(line, ctx, index) {
|
|
|
290
363
|
*/
|
|
291
364
|
export function handleTextarea(line, ctx, index) {
|
|
292
365
|
const match = line.match(/^:textarea\s+([A-Za-z_][\w-]*)\s*(.*)$/);
|
|
293
|
-
if (!match)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
366
|
+
if (!match) {
|
|
367
|
+
const hint = `:textarea name [placeholder="…"] [rows=N] [required] — e.g. ${TEXTAREA_EXAMPLE}`;
|
|
368
|
+
throw wdError(`Malformed :textarea in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
369
|
+
code: "WD417",
|
|
370
|
+
file: ctx.file,
|
|
371
|
+
line: lineOf(ctx, index),
|
|
372
|
+
hint,
|
|
373
|
+
example: TEXTAREA_EXAMPLE
|
|
374
|
+
});
|
|
375
|
+
}
|
|
297
376
|
const attrs = [`name="${escapeHtml(match[1])}"`];
|
|
298
377
|
let placeholder;
|
|
299
378
|
let hasAria = false;
|
|
@@ -301,7 +380,11 @@ export function handleTextarea(line, ctx, index) {
|
|
|
301
380
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
302
381
|
if (token[3]) {
|
|
303
382
|
if (!["required", "autofocus", "disabled", "readonly"].includes(token[3])) {
|
|
304
|
-
throw
|
|
383
|
+
throw wdError(`Unknown :textarea flag "${token[3]}" in ${at(ctx, index)}`, {
|
|
384
|
+
code: "WD418",
|
|
385
|
+
file: ctx.file,
|
|
386
|
+
line: lineOf(ctx, index)
|
|
387
|
+
});
|
|
305
388
|
}
|
|
306
389
|
attrs.push(token[3]);
|
|
307
390
|
continue;
|
|
@@ -319,7 +402,11 @@ export function handleTextarea(line, ctx, index) {
|
|
|
319
402
|
"aria-describedby"
|
|
320
403
|
].includes(token[1])
|
|
321
404
|
) {
|
|
322
|
-
throw
|
|
405
|
+
throw wdError(`Unknown :textarea attribute "${token[1]}" in ${at(ctx, index)}`, {
|
|
406
|
+
code: "WD419",
|
|
407
|
+
file: ctx.file,
|
|
408
|
+
line: lineOf(ctx, index)
|
|
409
|
+
});
|
|
323
410
|
}
|
|
324
411
|
if (token[1] === "placeholder") placeholder = value;
|
|
325
412
|
if (token[1] === "aria-label" || token[1] === "aria-describedby") hasAria = true;
|
|
@@ -343,24 +430,38 @@ export function handleTextarea(line, ctx, index) {
|
|
|
343
430
|
*/
|
|
344
431
|
export function handleSelect(line, optionLines, ctx, index) {
|
|
345
432
|
const match = line.match(/^:select\s+([A-Za-z_][\w-]*)\s*(.*)$/);
|
|
346
|
-
if (!match)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
433
|
+
if (!match) {
|
|
434
|
+
const hint = `:select name [required] then "- Label" lines — e.g. ${SELECT_EXAMPLE}`;
|
|
435
|
+
throw wdError(`Malformed :select in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
436
|
+
code: "WD420",
|
|
437
|
+
file: ctx.file,
|
|
438
|
+
line: lineOf(ctx, index),
|
|
439
|
+
hint,
|
|
440
|
+
example: SELECT_EXAMPLE
|
|
441
|
+
});
|
|
442
|
+
}
|
|
350
443
|
const attrs = [`name="${escapeHtml(match[1])}"`];
|
|
351
444
|
let hasAria = false;
|
|
352
445
|
const re = /([A-Za-z-]+)=("[^"]*"|\S+)|([A-Za-z-]+)/g;
|
|
353
446
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
354
447
|
if (token[3]) {
|
|
355
448
|
if (!["required", "disabled", "autofocus"].includes(token[3])) {
|
|
356
|
-
throw
|
|
449
|
+
throw wdError(`Unknown :select flag "${token[3]}" in ${at(ctx, index)}`, {
|
|
450
|
+
code: "WD421",
|
|
451
|
+
file: ctx.file,
|
|
452
|
+
line: lineOf(ctx, index)
|
|
453
|
+
});
|
|
357
454
|
}
|
|
358
455
|
attrs.push(token[3]);
|
|
359
456
|
continue;
|
|
360
457
|
}
|
|
361
458
|
const value = stripQuotes(token[2]);
|
|
362
459
|
if (!["autocomplete", "aria-label", "aria-describedby"].includes(token[1])) {
|
|
363
|
-
throw
|
|
460
|
+
throw wdError(`Unknown :select attribute "${token[1]}" in ${at(ctx, index)}`, {
|
|
461
|
+
code: "WD422",
|
|
462
|
+
file: ctx.file,
|
|
463
|
+
line: lineOf(ctx, index)
|
|
464
|
+
});
|
|
364
465
|
}
|
|
365
466
|
if (token[1] === "aria-label" || token[1] === "aria-describedby") hasAria = true;
|
|
366
467
|
attrs.push(`${token[1]}="${escapeHtml(value)}"`);
|
|
@@ -370,8 +471,9 @@ export function handleSelect(line, optionLines, ctx, index) {
|
|
|
370
471
|
}
|
|
371
472
|
const options = optionLines.map((l) => l.replace(/^\s*-\s+/, "").trim()).filter(Boolean);
|
|
372
473
|
if (!options.length) {
|
|
373
|
-
throw
|
|
374
|
-
`:select "${match[1]}" in ${at(ctx, index)} has no options. Add "- Label" lines beneath it
|
|
474
|
+
throw wdError(
|
|
475
|
+
`:select "${match[1]}" in ${at(ctx, index)} has no options. Add "- Label" lines beneath it.`,
|
|
476
|
+
{ code: "WD423", file: ctx.file, line: lineOf(ctx, index) }
|
|
375
477
|
);
|
|
376
478
|
}
|
|
377
479
|
const opts = options
|
|
@@ -404,10 +506,17 @@ export function handleChoiceGroup(line, optionLines, ctx, kind, index) {
|
|
|
404
506
|
? /^:checkbox\s+([A-Za-z_][\w-]*)\s*(.*)$/
|
|
405
507
|
: /^:radio\s+([A-Za-z_][\w-]*)\s*(.*)$/
|
|
406
508
|
);
|
|
407
|
-
if (!match)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
)
|
|
509
|
+
if (!match) {
|
|
510
|
+
const example = kind === "checkbox" ? CHECKBOX_EXAMPLE : RADIO_EXAMPLE;
|
|
511
|
+
const hint = `${directive} name [required] then "- Label" lines — e.g. ${example}`;
|
|
512
|
+
throw wdError(`Malformed ${directive} in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
513
|
+
code: "WD424",
|
|
514
|
+
file: ctx.file,
|
|
515
|
+
line: lineOf(ctx, index),
|
|
516
|
+
hint,
|
|
517
|
+
example
|
|
518
|
+
});
|
|
519
|
+
}
|
|
411
520
|
const name = match[1];
|
|
412
521
|
const flags = [];
|
|
413
522
|
let ariaLabel = null;
|
|
@@ -416,7 +525,11 @@ export function handleChoiceGroup(line, optionLines, ctx, kind, index) {
|
|
|
416
525
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
417
526
|
if (token[3]) {
|
|
418
527
|
if (!["required", "disabled", "autofocus"].includes(token[3])) {
|
|
419
|
-
throw
|
|
528
|
+
throw wdError(`Unknown ${directive} flag "${token[3]}" in ${at(ctx, index)}`, {
|
|
529
|
+
code: "WD425",
|
|
530
|
+
file: ctx.file,
|
|
531
|
+
line: lineOf(ctx, index)
|
|
532
|
+
});
|
|
420
533
|
}
|
|
421
534
|
flags.push(token[3]);
|
|
422
535
|
continue;
|
|
@@ -424,12 +537,18 @@ export function handleChoiceGroup(line, optionLines, ctx, kind, index) {
|
|
|
424
537
|
const value = stripQuotes(token[2]);
|
|
425
538
|
if (token[1] === "aria-label") ariaLabel = value;
|
|
426
539
|
else if (token[1] === "aria-describedby") ariaDescribedby = value;
|
|
427
|
-
else
|
|
540
|
+
else
|
|
541
|
+
throw wdError(`Unknown ${directive} attribute "${token[1]}" in ${at(ctx, index)}`, {
|
|
542
|
+
code: "WD426",
|
|
543
|
+
file: ctx.file,
|
|
544
|
+
line: lineOf(ctx, index)
|
|
545
|
+
});
|
|
428
546
|
}
|
|
429
547
|
const options = optionLines.map((l) => l.replace(/^\s*-\s+/, "").trim()).filter(Boolean);
|
|
430
548
|
if (!options.length) {
|
|
431
|
-
throw
|
|
432
|
-
`${directive} "${name}" in ${at(ctx, index)} has no options. Add "- Label" lines beneath it
|
|
549
|
+
throw wdError(
|
|
550
|
+
`${directive} "${name}" in ${at(ctx, index)} has no options. Add "- Label" lines beneath it.`,
|
|
551
|
+
{ code: "WD427", file: ctx.file, line: lineOf(ctx, index) }
|
|
433
552
|
);
|
|
434
553
|
}
|
|
435
554
|
const isCheckbox = kind === "checkbox";
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// values, and warn when a file looks like it forgot the opening `---`.
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
|
|
6
|
+
import { normalizeNewlines, wdError } from "./context.js";
|
|
6
7
|
import { stripQuotes } from "./interpolation.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -20,12 +21,20 @@ import { stripQuotes } from "./interpolation.js";
|
|
|
20
21
|
* @returns {{ meta: Meta, body: string, bodyLine: number }}
|
|
21
22
|
*/
|
|
22
23
|
export function parseFrontmatter(raw, file) {
|
|
24
|
+
// A file authored on Windows (or checked out with git's autocrlf) arrives
|
|
25
|
+
// CRLF-terminated, and every delimiter/line test below is LF-shaped — so
|
|
26
|
+
// without this the opening `---\r\n` never matches and the whole frontmatter
|
|
27
|
+
// block is silently treated as body text. Normalizing here rather than only
|
|
28
|
+
// in the reader keeps the direct callers (builder.js reads route frontmatter
|
|
29
|
+
// straight off disk for feeds) on the same footing. Idempotent on LF input.
|
|
30
|
+
raw = normalizeNewlines(raw);
|
|
23
31
|
if (!raw.startsWith("---\n")) return { meta: {}, body: raw, bodyLine: 0 };
|
|
24
32
|
const end = raw.indexOf("\n---", 3);
|
|
25
33
|
if (end === -1) {
|
|
26
34
|
const where = file ? ` in ${file}` : "";
|
|
27
|
-
throw
|
|
28
|
-
`Unterminated frontmatter${where}: opening "---" has no closing "---". Use: --- on its own line to open and another --- to close, then the page body
|
|
35
|
+
throw wdError(
|
|
36
|
+
`Unterminated frontmatter${where}: opening "---" has no closing "---". Use: --- on its own line to open and another --- to close, then the page body.`,
|
|
37
|
+
{ code: "WD001", file }
|
|
29
38
|
);
|
|
30
39
|
}
|
|
31
40
|
const front = raw.slice(4, end).trim();
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
|
+
import { normalizeNewlines } from "./context.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* A {@link import("./reader.js").Reader} backed by `node:fs`. Each method is a
|
|
@@ -17,7 +18,7 @@ import fs from "node:fs";
|
|
|
17
18
|
*/
|
|
18
19
|
export function fsReader() {
|
|
19
20
|
return {
|
|
20
|
-
readText: (absPath) => fs.readFileSync(absPath, "utf8"),
|
|
21
|
+
readText: (absPath) => normalizeNewlines(fs.readFileSync(absPath, "utf8")),
|
|
21
22
|
readBinary: (absPath) => fs.readFileSync(absPath),
|
|
22
23
|
exists: (absPath) => fs.existsSync(absPath),
|
|
23
24
|
realpath: (absPath) => fs.realpathSync(absPath)
|