@zvndev/darkmown 2.2.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 +13 -0
- package/README.md +34 -2
- package/docs/spec-alignment.md +10 -0
- package/package.json +16 -11
- package/site/pages/docs/index.wd +14 -0
- package/site/pages/playground/index.js +47 -46
- package/src/catalog.js +16 -1
- package/src/compiler/actions.js +42 -22
- package/src/compiler/body.js +36 -14
- package/src/compiler/collections.js +16 -10
- package/src/compiler/context.js +39 -10
- package/src/compiler/fetch.js +39 -15
- package/src/compiler/format.js +5 -2
- package/src/compiler/forms.js +91 -26
- 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 +63 -35
- package/src/compiler/markdown.js +4 -3
- package/src/compiler/media.js +17 -3
- package/src/compiler/page.js +8 -6
- package/src/compiler/predicates.js +49 -24
- package/src/compiler/reader.js +12 -2
- package/src/compiler/state.js +16 -2
- package/src/compiler/structure.js +24 -14
- package/src/deploy.js +8 -4
- package/src/errors.js +1156 -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/forms.js
CHANGED
|
@@ -59,6 +59,7 @@ export function handleForm(line, bodyLines, ctx, index) {
|
|
|
59
59
|
if ((!rawAction && !into) || leftover) {
|
|
60
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
61
|
throw wdError(`Malformed :form in ${at(ctx, index)}: ${line}. Use ${hint}`, {
|
|
62
|
+
code: "WD401",
|
|
62
63
|
file: ctx.file,
|
|
63
64
|
line: lineOf(ctx, index),
|
|
64
65
|
hint,
|
|
@@ -88,6 +89,7 @@ export function handleInput(line, ctx, index) {
|
|
|
88
89
|
const match = line.match(/^:input\s+([A-Za-z_][\w-]*)\s*(.*)$/);
|
|
89
90
|
if (!match)
|
|
90
91
|
throw wdError(`Malformed :input in ${at(ctx, index)}: ${line}. ${INPUT_USE}`, {
|
|
92
|
+
code: "WD402",
|
|
91
93
|
file: ctx.file,
|
|
92
94
|
line: lineOf(ctx, index),
|
|
93
95
|
hint: INPUT_USE.slice("Use: ".length),
|
|
@@ -101,7 +103,11 @@ export function handleInput(line, ctx, index) {
|
|
|
101
103
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
102
104
|
if (token[3]) {
|
|
103
105
|
if (!["required", "autofocus", "disabled", "readonly"].includes(token[3])) {
|
|
104
|
-
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
|
+
});
|
|
105
111
|
}
|
|
106
112
|
attrs.push(token[3]);
|
|
107
113
|
continue;
|
|
@@ -124,7 +130,11 @@ export function handleInput(line, ctx, index) {
|
|
|
124
130
|
"aria-describedby"
|
|
125
131
|
].includes(token[1])
|
|
126
132
|
) {
|
|
127
|
-
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
|
+
});
|
|
128
138
|
}
|
|
129
139
|
if (token[1] === "placeholder") placeholder = value;
|
|
130
140
|
if (token[1] === "aria-label" || token[1] === "aria-describedby") hasAria = true;
|
|
@@ -152,6 +162,7 @@ export function handleBind(line, ctx, index) {
|
|
|
152
162
|
const match = line.match(/^:bind\s+([A-Za-z_$][\w$]*)\s*(.*)$/);
|
|
153
163
|
if (!match)
|
|
154
164
|
throw wdError(`Malformed :bind in ${at(ctx, index)}: ${line}. Use: ${BIND_EXAMPLE}`, {
|
|
165
|
+
code: "WD405",
|
|
155
166
|
file: ctx.file,
|
|
156
167
|
line: lineOf(ctx, index),
|
|
157
168
|
hint: BIND_EXAMPLE,
|
|
@@ -159,8 +170,9 @@ export function handleBind(line, ctx, index) {
|
|
|
159
170
|
});
|
|
160
171
|
const key = resolveStateKey(match[1], ctx);
|
|
161
172
|
if (!key) {
|
|
162
|
-
throw
|
|
163
|
-
`: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 }
|
|
164
176
|
);
|
|
165
177
|
}
|
|
166
178
|
ctx.comp.assets.runtime = true;
|
|
@@ -172,7 +184,11 @@ export function handleBind(line, ctx, index) {
|
|
|
172
184
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
173
185
|
if (token[3]) {
|
|
174
186
|
if (!["required", "autofocus"].includes(token[3]))
|
|
175
|
-
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
|
+
});
|
|
176
192
|
attrs.push(token[3]);
|
|
177
193
|
continue;
|
|
178
194
|
}
|
|
@@ -182,7 +198,11 @@ export function handleBind(line, ctx, index) {
|
|
|
182
198
|
continue;
|
|
183
199
|
}
|
|
184
200
|
if (!["placeholder", "autocomplete", "aria-label", "aria-describedby"].includes(token[1])) {
|
|
185
|
-
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
|
+
});
|
|
186
206
|
}
|
|
187
207
|
if (token[1] === "placeholder") placeholder = value;
|
|
188
208
|
if (token[1] === "aria-label" || token[1] === "aria-describedby") hasAria = true;
|
|
@@ -215,6 +235,7 @@ export function handleSlider(line, ctx, index) {
|
|
|
215
235
|
const head = line.match(/^:slider\s+([A-Za-z_$][\w$]*)\s*(.*)$/);
|
|
216
236
|
if (!head)
|
|
217
237
|
throw wdError(`Malformed :slider in ${at(ctx, index)}: ${line}. Use: ${SLIDER_EXAMPLE}`, {
|
|
238
|
+
code: "WD409",
|
|
218
239
|
file: ctx.file,
|
|
219
240
|
line: lineOf(ctx, index),
|
|
220
241
|
hint: SLIDER_EXAMPLE,
|
|
@@ -228,8 +249,9 @@ export function handleSlider(line, ctx, index) {
|
|
|
228
249
|
if (rest.startsWith("=")) {
|
|
229
250
|
const valueMatch = rest.match(/^=\s*(\S+)\s*(.*)$/);
|
|
230
251
|
if (!valueMatch)
|
|
231
|
-
throw
|
|
232
|
-
`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) }
|
|
233
255
|
);
|
|
234
256
|
initialRaw = valueMatch[1];
|
|
235
257
|
rest = valueMatch[2].trim();
|
|
@@ -251,7 +273,12 @@ export function handleSlider(line, ctx, index) {
|
|
|
251
273
|
else if (token[1] === "max") max = value;
|
|
252
274
|
else if (token[1] === "step") step = value;
|
|
253
275
|
else if (token[1] === "aria-label") ariaLabel = value;
|
|
254
|
-
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
|
+
});
|
|
255
282
|
}
|
|
256
283
|
for (const [label, raw] of [
|
|
257
284
|
["min", min],
|
|
@@ -259,7 +286,11 @@ export function handleSlider(line, ctx, index) {
|
|
|
259
286
|
["step", step]
|
|
260
287
|
]) {
|
|
261
288
|
if (!/^-?\d+(?:\.\d+)?$/.test(raw))
|
|
262
|
-
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
|
+
});
|
|
263
294
|
}
|
|
264
295
|
|
|
265
296
|
ctx.comp.assets.runtime = true;
|
|
@@ -269,21 +300,24 @@ export function handleSlider(line, ctx, index) {
|
|
|
269
300
|
if (initialRaw !== undefined) {
|
|
270
301
|
const value = parseStateValue(initialRaw, at(ctx, index));
|
|
271
302
|
if (typeof value !== "number")
|
|
272
|
-
throw
|
|
273
|
-
`: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) }
|
|
274
306
|
);
|
|
275
307
|
key = declareState(name, value, ctx);
|
|
276
308
|
const persistAttr = persist ? ` data-wd-persist="${key}"` : "";
|
|
277
309
|
seed = `<script type="application/json" data-wd-state${persistAttr}>${safeScriptJson({ [key]: value })}</script>`;
|
|
278
310
|
} else {
|
|
279
311
|
if (persist)
|
|
280
|
-
throw
|
|
281
|
-
`: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) }
|
|
282
315
|
);
|
|
283
316
|
const resolved = resolveStateKey(name, ctx);
|
|
284
317
|
if (!resolved)
|
|
285
|
-
throw
|
|
286
|
-
`: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 }
|
|
287
321
|
);
|
|
288
322
|
key = resolved;
|
|
289
323
|
}
|
|
@@ -307,6 +341,7 @@ export function handleSubmit(line, ctx, index) {
|
|
|
307
341
|
throw wdError(
|
|
308
342
|
`Malformed :submit in ${at(ctx, index)}: ${line}. Use: :submit "Label" — e.g. ${SUBMIT_EXAMPLE}`,
|
|
309
343
|
{
|
|
344
|
+
code: "WD416",
|
|
310
345
|
file: ctx.file,
|
|
311
346
|
line: lineOf(ctx, index),
|
|
312
347
|
hint: `:submit "Label" — e.g. ${SUBMIT_EXAMPLE}`,
|
|
@@ -331,6 +366,7 @@ export function handleTextarea(line, ctx, index) {
|
|
|
331
366
|
if (!match) {
|
|
332
367
|
const hint = `:textarea name [placeholder="…"] [rows=N] [required] — e.g. ${TEXTAREA_EXAMPLE}`;
|
|
333
368
|
throw wdError(`Malformed :textarea in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
369
|
+
code: "WD417",
|
|
334
370
|
file: ctx.file,
|
|
335
371
|
line: lineOf(ctx, index),
|
|
336
372
|
hint,
|
|
@@ -344,7 +380,11 @@ export function handleTextarea(line, ctx, index) {
|
|
|
344
380
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
345
381
|
if (token[3]) {
|
|
346
382
|
if (!["required", "autofocus", "disabled", "readonly"].includes(token[3])) {
|
|
347
|
-
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
|
+
});
|
|
348
388
|
}
|
|
349
389
|
attrs.push(token[3]);
|
|
350
390
|
continue;
|
|
@@ -362,7 +402,11 @@ export function handleTextarea(line, ctx, index) {
|
|
|
362
402
|
"aria-describedby"
|
|
363
403
|
].includes(token[1])
|
|
364
404
|
) {
|
|
365
|
-
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
|
+
});
|
|
366
410
|
}
|
|
367
411
|
if (token[1] === "placeholder") placeholder = value;
|
|
368
412
|
if (token[1] === "aria-label" || token[1] === "aria-describedby") hasAria = true;
|
|
@@ -389,6 +433,7 @@ export function handleSelect(line, optionLines, ctx, index) {
|
|
|
389
433
|
if (!match) {
|
|
390
434
|
const hint = `:select name [required] then "- Label" lines — e.g. ${SELECT_EXAMPLE}`;
|
|
391
435
|
throw wdError(`Malformed :select in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
436
|
+
code: "WD420",
|
|
392
437
|
file: ctx.file,
|
|
393
438
|
line: lineOf(ctx, index),
|
|
394
439
|
hint,
|
|
@@ -401,14 +446,22 @@ export function handleSelect(line, optionLines, ctx, index) {
|
|
|
401
446
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
402
447
|
if (token[3]) {
|
|
403
448
|
if (!["required", "disabled", "autofocus"].includes(token[3])) {
|
|
404
|
-
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
|
+
});
|
|
405
454
|
}
|
|
406
455
|
attrs.push(token[3]);
|
|
407
456
|
continue;
|
|
408
457
|
}
|
|
409
458
|
const value = stripQuotes(token[2]);
|
|
410
459
|
if (!["autocomplete", "aria-label", "aria-describedby"].includes(token[1])) {
|
|
411
|
-
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
|
+
});
|
|
412
465
|
}
|
|
413
466
|
if (token[1] === "aria-label" || token[1] === "aria-describedby") hasAria = true;
|
|
414
467
|
attrs.push(`${token[1]}="${escapeHtml(value)}"`);
|
|
@@ -418,8 +471,9 @@ export function handleSelect(line, optionLines, ctx, index) {
|
|
|
418
471
|
}
|
|
419
472
|
const options = optionLines.map((l) => l.replace(/^\s*-\s+/, "").trim()).filter(Boolean);
|
|
420
473
|
if (!options.length) {
|
|
421
|
-
throw
|
|
422
|
-
`: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) }
|
|
423
477
|
);
|
|
424
478
|
}
|
|
425
479
|
const opts = options
|
|
@@ -456,6 +510,7 @@ export function handleChoiceGroup(line, optionLines, ctx, kind, index) {
|
|
|
456
510
|
const example = kind === "checkbox" ? CHECKBOX_EXAMPLE : RADIO_EXAMPLE;
|
|
457
511
|
const hint = `${directive} name [required] then "- Label" lines — e.g. ${example}`;
|
|
458
512
|
throw wdError(`Malformed ${directive} in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
|
|
513
|
+
code: "WD424",
|
|
459
514
|
file: ctx.file,
|
|
460
515
|
line: lineOf(ctx, index),
|
|
461
516
|
hint,
|
|
@@ -470,7 +525,11 @@ export function handleChoiceGroup(line, optionLines, ctx, kind, index) {
|
|
|
470
525
|
for (const token of (match[2] || "").matchAll(re)) {
|
|
471
526
|
if (token[3]) {
|
|
472
527
|
if (!["required", "disabled", "autofocus"].includes(token[3])) {
|
|
473
|
-
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
|
+
});
|
|
474
533
|
}
|
|
475
534
|
flags.push(token[3]);
|
|
476
535
|
continue;
|
|
@@ -478,12 +537,18 @@ export function handleChoiceGroup(line, optionLines, ctx, kind, index) {
|
|
|
478
537
|
const value = stripQuotes(token[2]);
|
|
479
538
|
if (token[1] === "aria-label") ariaLabel = value;
|
|
480
539
|
else if (token[1] === "aria-describedby") ariaDescribedby = value;
|
|
481
|
-
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
|
+
});
|
|
482
546
|
}
|
|
483
547
|
const options = optionLines.map((l) => l.replace(/^\s*-\s+/, "").trim()).filter(Boolean);
|
|
484
548
|
if (!options.length) {
|
|
485
|
-
throw
|
|
486
|
-
`${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) }
|
|
487
552
|
);
|
|
488
553
|
}
|
|
489
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)
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Intrinsic image dimensions, read straight from the file header.
|
|
3
|
+
//
|
|
4
|
+
// `enhanceImages` needs one number pair per `<img>` — the width and height the
|
|
5
|
+
// browser reserves so the page never reflows as an image decodes. That is a
|
|
6
|
+
// header read, not image decoding: every format we care about states its size
|
|
7
|
+
// in the first few dozen bytes. This module does exactly that much and nothing
|
|
8
|
+
// else, which is why it replaced the `image-size` dependency (whose ICNS/JXL/
|
|
9
|
+
// HEIF parsers ship a live denial-of-service advisory with no fixed release —
|
|
10
|
+
// unbounded loops on malformed input). We parse only the formats a Darkmown
|
|
11
|
+
// site actually references, and every loop here is bounded by the buffer
|
|
12
|
+
// length, so a truncated or hostile file terminates instead of spinning.
|
|
13
|
+
//
|
|
14
|
+
// Supported: PNG (including Apple's "fried" CgBI variant), JPEG (behind any
|
|
15
|
+
// number of leading APP/EXIF segments), GIF, WebP (VP8 lossy, VP8L lossless,
|
|
16
|
+
// VP8X extended), and SVG (root width/height in any CSS absolute unit, falling
|
|
17
|
+
// back to the viewBox). Anything else — and anything malformed — returns null,
|
|
18
|
+
// which `measureImage` treats exactly like a missing file: emit no dimensions
|
|
19
|
+
// and move on. A build must never fail because of an image.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {{ width: number, height: number }} Dimensions Intrinsic pixel size.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** PNG's 8-byte file signature. */
|
|
27
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
28
|
+
|
|
29
|
+
/** The chunk Apple's pngcrush (Xcode asset pipeline) inserts AHEAD of the IHDR,
|
|
30
|
+
* the one legal-in-the-wild violation of "IHDR is the first chunk". */
|
|
31
|
+
const PNG_FRIED_CHUNK = "CgBI";
|
|
32
|
+
|
|
33
|
+
/** The two GIF header versions in existence. */
|
|
34
|
+
const GIF_SIGNATURES = new Set(["GIF87a", "GIF89a"]);
|
|
35
|
+
|
|
36
|
+
/** How much of an unrecognized file to decode as text when sniffing for SVG.
|
|
37
|
+
* An SVG's root element is always in the first few hundred bytes; capping the
|
|
38
|
+
* decode keeps the scan constant-time on a large unknown binary. */
|
|
39
|
+
const SVG_SCAN_BYTES = 4096;
|
|
40
|
+
|
|
41
|
+
/** CSS absolute length units, in pixels. Font-relative units (`em`, `ex`, `rem`)
|
|
42
|
+
* and percentages depend on layout we do not have, so they are deliberately
|
|
43
|
+
* ABSENT: such an SVG falls back to its viewBox instead of to a guess. */
|
|
44
|
+
/** @type {Record<string, number>} */
|
|
45
|
+
const CSS_UNITS = {
|
|
46
|
+
px: 1,
|
|
47
|
+
pt: 96 / 72,
|
|
48
|
+
pc: 16,
|
|
49
|
+
in: 96,
|
|
50
|
+
cm: 96 / 2.54,
|
|
51
|
+
mm: 96 / 25.4,
|
|
52
|
+
q: 96 / 101.6
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read an image's intrinsic dimensions from its header bytes.
|
|
57
|
+
* @param {Uint8Array} bytes Raw file contents (a Node `Buffer` is a `Uint8Array`).
|
|
58
|
+
* @returns {Dimensions | null} The size, or null when the bytes are empty, of an
|
|
59
|
+
* unsupported format, or malformed. Never throws.
|
|
60
|
+
*/
|
|
61
|
+
export function imageSize(bytes) {
|
|
62
|
+
if (!(bytes instanceof Uint8Array) || bytes.length === 0) return null;
|
|
63
|
+
// Ordered cheapest-magic-first; SVG is last because it is the only one that
|
|
64
|
+
// has to decode text rather than compare a fixed byte signature.
|
|
65
|
+
return png(bytes) ?? jpeg(bytes) ?? gif(bytes) ?? webp(bytes) ?? svg(bytes);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* PNG: the IHDR chunk is mandated to be the FIRST chunk, and carries the size
|
|
70
|
+
* as two big-endian 32-bit fields. The one exception in the wild is an Apple
|
|
71
|
+
* "fried" PNG, which pushes IHDR behind a `CgBI` chunk; skip that by its own
|
|
72
|
+
* declared length rather than a hardcoded offset, and let the bounds check
|
|
73
|
+
* below reject a length that points outside the file.
|
|
74
|
+
* @param {Uint8Array} b
|
|
75
|
+
* @returns {Dimensions | null}
|
|
76
|
+
*/
|
|
77
|
+
function png(b) {
|
|
78
|
+
if (!matches(b, PNG_SIGNATURE, 0)) return null;
|
|
79
|
+
// Chunk framing: 4-byte length, 4-byte type, payload, 4-byte CRC.
|
|
80
|
+
let at = 8;
|
|
81
|
+
if (b.length >= 16 && ascii(b, 12, 4) === PNG_FRIED_CHUNK) at += 12 + u32be(b, 8);
|
|
82
|
+
if (b.length < at + 16 || ascii(b, at + 4, 4) !== "IHDR") return null;
|
|
83
|
+
return dimensions(u32be(b, at + 8), u32be(b, at + 12));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* JPEG: the size lives in a Start-Of-Frame segment, which sits behind an
|
|
88
|
+
* arbitrary run of APP0/APP1(EXIF)/COM/quantization segments, so the header has
|
|
89
|
+
* to be walked. The walk is bounded twice over: `offset` only ever grows (by at
|
|
90
|
+
* least one byte per pass), and the loop condition is the buffer length — a
|
|
91
|
+
* truncated or self-contradictory file falls out instead of looping forever.
|
|
92
|
+
* @param {Uint8Array} b
|
|
93
|
+
* @returns {Dimensions | null}
|
|
94
|
+
*/
|
|
95
|
+
function jpeg(b) {
|
|
96
|
+
if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null;
|
|
97
|
+
let offset = 2;
|
|
98
|
+
while (offset + 1 < b.length) {
|
|
99
|
+
// Every segment begins at a marker boundary. Anything else means we lost
|
|
100
|
+
// sync with the segment chain and can no longer trust our offsets.
|
|
101
|
+
if (b[offset] !== 0xff) return null;
|
|
102
|
+
const marker = b[offset + 1];
|
|
103
|
+
// A run of 0xFF bytes is legal padding before the real marker type.
|
|
104
|
+
if (marker === 0xff) {
|
|
105
|
+
offset += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
offset += 2;
|
|
109
|
+
// Standalone markers carry no payload: TEM (0x01) and the RST0-7 restarts.
|
|
110
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) continue;
|
|
111
|
+
// End-of-image, or the start of entropy-coded scan data: past this point
|
|
112
|
+
// there is no more header to read, so a missing SOF is simply unknowable.
|
|
113
|
+
if (marker === 0xd9 || marker === 0xda) return null;
|
|
114
|
+
if (offset + 1 >= b.length) return null;
|
|
115
|
+
const length = u16be(b, offset);
|
|
116
|
+
// The length field counts itself, and must fit inside the file — a segment
|
|
117
|
+
// claiming to run past the end is the malformed case, not a short read.
|
|
118
|
+
if (length < 2 || offset + length > b.length) return null;
|
|
119
|
+
// SOF0-SOF15 hold the frame dimensions. C4/C8/CC share the range but are
|
|
120
|
+
// DHT / JPG-extension / DAC segments, not frame headers.
|
|
121
|
+
if (isStartOfFrame(marker)) {
|
|
122
|
+
// precision (1) + height (2) + width (2) after the length field itself.
|
|
123
|
+
if (length < 7) return null;
|
|
124
|
+
return dimensions(u16be(b, offset + 5), u16be(b, offset + 3));
|
|
125
|
+
}
|
|
126
|
+
offset += length;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Whether a JPEG marker is a Start-Of-Frame (the segment carrying dimensions).
|
|
133
|
+
* @param {number} marker
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
function isStartOfFrame(marker) {
|
|
137
|
+
if (marker < 0xc0 || marker > 0xcf) return false;
|
|
138
|
+
return marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* GIF: the logical screen descriptor follows the 6-byte header immediately,
|
|
143
|
+
* with little-endian 16-bit dimensions.
|
|
144
|
+
* @param {Uint8Array} b
|
|
145
|
+
* @returns {Dimensions | null}
|
|
146
|
+
*/
|
|
147
|
+
function gif(b) {
|
|
148
|
+
if (b.length < 10 || !GIF_SIGNATURES.has(ascii(b, 0, 6))) return null;
|
|
149
|
+
return dimensions(u16le(b, 6), u16le(b, 8));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* WebP: a RIFF container whose first chunk identifies the coding variant, each
|
|
154
|
+
* of which stores the canvas size differently.
|
|
155
|
+
* @param {Uint8Array} b
|
|
156
|
+
* @returns {Dimensions | null}
|
|
157
|
+
*/
|
|
158
|
+
function webp(b) {
|
|
159
|
+
if (b.length < 16 || ascii(b, 0, 4) !== "RIFF" || ascii(b, 8, 4) !== "WEBP") return null;
|
|
160
|
+
const chunk = ascii(b, 12, 4);
|
|
161
|
+
// Lossy: a 3-byte frame tag, the 9D 01 2A sync code, then 14-bit dimensions
|
|
162
|
+
// (the top two bits of each 16-bit field are a scaling hint, not size).
|
|
163
|
+
if (chunk === "VP8 ") {
|
|
164
|
+
if (b.length < 30 || b[23] !== 0x9d || b[24] !== 0x01 || b[25] !== 0x2a) return null;
|
|
165
|
+
return dimensions(u16le(b, 26) & 0x3fff, u16le(b, 28) & 0x3fff);
|
|
166
|
+
}
|
|
167
|
+
// Lossless: a 0x2F signature byte, then (width - 1) in the low 14 bits and
|
|
168
|
+
// (height - 1) in the next 14 of one little-endian 32-bit word.
|
|
169
|
+
if (chunk === "VP8L") {
|
|
170
|
+
if (b.length < 25 || b[20] !== 0x2f) return null;
|
|
171
|
+
const bits = u32le(b, 21);
|
|
172
|
+
return dimensions((bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1);
|
|
173
|
+
}
|
|
174
|
+
// Extended (alpha / animation / ICC): the canvas size as two 3-byte
|
|
175
|
+
// little-endian (value - 1) fields, after the flag byte and 3 reserved bytes.
|
|
176
|
+
if (chunk === "VP8X") {
|
|
177
|
+
if (b.length < 30) return null;
|
|
178
|
+
return dimensions(u24le(b, 24) + 1, u24le(b, 27) + 1);
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* SVG: markup, not a binary header. The intrinsic size is the root element's
|
|
185
|
+
* `width`/`height`; when those are absent or expressed in units we cannot
|
|
186
|
+
* resolve to pixels (percentages, `em`), the `viewBox`'s third and fourth
|
|
187
|
+
* numbers are the intrinsic size — that is the fallback browsers use too.
|
|
188
|
+
* @param {Uint8Array} b
|
|
189
|
+
* @returns {Dimensions | null}
|
|
190
|
+
*/
|
|
191
|
+
function svg(b) {
|
|
192
|
+
const head = decodeHead(b);
|
|
193
|
+
// Markup starts with `<` once a BOM / whitespace / XML declaration is past.
|
|
194
|
+
// Bailing here keeps an unrecognized binary from being scanned as text.
|
|
195
|
+
if (!/^\s*</.test(head)) return null;
|
|
196
|
+
const open = head.match(/<svg\b[^>]*>/i);
|
|
197
|
+
if (!open) return null;
|
|
198
|
+
const width = svgLength(attr(open[0], "width"));
|
|
199
|
+
const height = svgLength(attr(open[0], "height"));
|
|
200
|
+
if (width !== null && height !== null) return dimensions(width, height);
|
|
201
|
+
const box = viewBox(open[0]);
|
|
202
|
+
if (!box) return null;
|
|
203
|
+
// With one axis given, the viewBox supplies the aspect ratio for the other —
|
|
204
|
+
// how a browser resolves an `auto` dimension. With neither, the viewBox is
|
|
205
|
+
// itself the intrinsic size.
|
|
206
|
+
if (width !== null) return dimensions(width, (width * box.height) / box.width);
|
|
207
|
+
if (height !== null) return dimensions((height * box.width) / box.height, height);
|
|
208
|
+
return dimensions(box.width, box.height);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The width/height of an SVG root's `viewBox` (its third and fourth numbers).
|
|
213
|
+
* Per the spec the four values are separated by whitespace and/or a comma.
|
|
214
|
+
* @param {string} tag The `<svg …>` opening tag.
|
|
215
|
+
* @returns {Dimensions | null}
|
|
216
|
+
*/
|
|
217
|
+
function viewBox(tag) {
|
|
218
|
+
const raw = attr(tag, "viewBox");
|
|
219
|
+
if (raw === null) return null;
|
|
220
|
+
const parts = raw.trim().split(/[\s,]+/);
|
|
221
|
+
if (parts.length !== 4) return null;
|
|
222
|
+
const width = Number(parts[2]);
|
|
223
|
+
const height = Number(parts[3]);
|
|
224
|
+
// Written as `!(x > 0)` so NaN — a non-numeric viewBox — is rejected too.
|
|
225
|
+
if (!(width > 0) || !(height > 0)) return null;
|
|
226
|
+
return { width, height };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Read a quoted attribute value out of a single opening tag. The name must be
|
|
231
|
+
* preceded by whitespace or a quote so `width` never matches `stroke-width`
|
|
232
|
+
* (the same guard `ensureMainLandmark` uses for `id` vs `data-id`). Unquoted
|
|
233
|
+
* values are not matched — rare in SVG, and the viewBox fallback covers them.
|
|
234
|
+
* @param {string} tag One opening tag, `<` through `>`.
|
|
235
|
+
* @param {string} name Attribute name (an internal constant, never user input).
|
|
236
|
+
* @returns {string | null}
|
|
237
|
+
*/
|
|
238
|
+
function attr(tag, name) {
|
|
239
|
+
const m = tag.match(new RegExp(`[\\s"']${name}\\s*=\\s*["']([^"']*)["']`, "i"));
|
|
240
|
+
return m ? m[1] : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Parse an SVG length into CSS pixels. Unitless values are pixels, and the CSS
|
|
245
|
+
* absolute units convert exactly ({@link CSS_UNITS} — `pt` is common in
|
|
246
|
+
* Graphviz/Illustrator output). A percentage or a font-relative unit needs
|
|
247
|
+
* layout we do not have, so it returns null and the caller falls back to the
|
|
248
|
+
* viewBox — which is what a browser effectively does.
|
|
249
|
+
* @param {string | null} value
|
|
250
|
+
* @returns {number | null}
|
|
251
|
+
*/
|
|
252
|
+
function svgLength(value) {
|
|
253
|
+
if (value === null) return null;
|
|
254
|
+
const m = /^\s*([0-9]*\.?[0-9]+)\s*([a-z]*)\s*$/i.exec(value);
|
|
255
|
+
if (!m) return null;
|
|
256
|
+
const scale = m[2] ? CSS_UNITS[m[2].toLowerCase()] : 1;
|
|
257
|
+
return scale === undefined ? null : Number(m[1]) * scale;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Decode the leading bytes of a file as UTF-8 text for the SVG sniff. Capped at
|
|
262
|
+
* {@link SVG_SCAN_BYTES}; a truncated multi-byte sequence at the cap decodes to
|
|
263
|
+
* a replacement character rather than throwing.
|
|
264
|
+
* @param {Uint8Array} b
|
|
265
|
+
* @returns {string}
|
|
266
|
+
*/
|
|
267
|
+
function decodeHead(b) {
|
|
268
|
+
return new TextDecoder("utf-8").decode(b.subarray(0, SVG_SCAN_BYTES));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validate and normalize a parsed pair into the public shape. Non-numeric,
|
|
273
|
+
* non-finite, and non-positive results are rejected — a zero or NaN dimension
|
|
274
|
+
* on an `<img>` is worse than none at all.
|
|
275
|
+
* @param {number} width
|
|
276
|
+
* @param {number} height
|
|
277
|
+
* @returns {Dimensions | null}
|
|
278
|
+
*/
|
|
279
|
+
function dimensions(width, height) {
|
|
280
|
+
const w = Math.round(width);
|
|
281
|
+
const h = Math.round(height);
|
|
282
|
+
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return null;
|
|
283
|
+
return { width: w, height: h };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Whether the buffer carries an exact byte sequence at an offset.
|
|
288
|
+
* @param {Uint8Array} b
|
|
289
|
+
* @param {number[]} signature
|
|
290
|
+
* @param {number} offset
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
function matches(b, signature, offset) {
|
|
294
|
+
if (b.length < offset + signature.length) return false;
|
|
295
|
+
for (let i = 0; i < signature.length; i++) {
|
|
296
|
+
if (b[offset + i] !== signature[i]) return false;
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Read a fixed-length run of bytes as a Latin-1 string (format four-character
|
|
303
|
+
* codes: `IHDR`, `RIFF`, `VP8L`, …). The caller guarantees the range is inside
|
|
304
|
+
* the buffer.
|
|
305
|
+
* @param {Uint8Array} b
|
|
306
|
+
* @param {number} offset
|
|
307
|
+
* @param {number} length
|
|
308
|
+
* @returns {string}
|
|
309
|
+
*/
|
|
310
|
+
function ascii(b, offset, length) {
|
|
311
|
+
let out = "";
|
|
312
|
+
for (let i = 0; i < length; i++) out += String.fromCharCode(b[offset + i]);
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** @param {Uint8Array} b @param {number} i @returns {number} Big-endian uint16. */
|
|
317
|
+
function u16be(b, i) {
|
|
318
|
+
return (b[i] << 8) | b[i + 1];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** @param {Uint8Array} b @param {number} i @returns {number} Little-endian uint16. */
|
|
322
|
+
function u16le(b, i) {
|
|
323
|
+
return b[i] | (b[i + 1] << 8);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** @param {Uint8Array} b @param {number} i @returns {number} Big-endian uint32. */
|
|
327
|
+
function u32be(b, i) {
|
|
328
|
+
// Multiply rather than shift the top byte: `<< 24` would go negative.
|
|
329
|
+
return b[i] * 0x1000000 + ((b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** @param {Uint8Array} b @param {number} i @returns {number} Little-endian uint32. */
|
|
333
|
+
function u32le(b, i) {
|
|
334
|
+
return (b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)) >>> 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** @param {Uint8Array} b @param {number} i @returns {number} Little-endian uint24. */
|
|
338
|
+
function u24le(b, i) {
|
|
339
|
+
return b[i] | (b[i + 1] << 8) | (b[i + 2] << 16);
|
|
340
|
+
}
|