@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
|
@@ -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
|
+
}
|
package/src/compiler/includes.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { scopeIdFor } from "../skin.js";
|
|
10
|
-
import { pageIncludeExtensions } from "./context.js";
|
|
10
|
+
import { pageIncludeExtensions, wdError } from "./context.js";
|
|
11
11
|
import { stripQuotes } from "./interpolation.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -181,14 +181,18 @@ export function resolveInclude(spec, fromFile, context, allowAny, loc, reader) {
|
|
|
181
181
|
for (const candidate of candidates) {
|
|
182
182
|
const resolved = path.resolve(candidate);
|
|
183
183
|
if (!isAllowedInclude(resolved, context)) {
|
|
184
|
-
throw
|
|
184
|
+
throw wdError(`Include "${spec}" from ${loc} resolves outside site/pages or site/_`, {
|
|
185
|
+
code: "WD601",
|
|
186
|
+
file: fromFile
|
|
187
|
+
});
|
|
185
188
|
}
|
|
186
189
|
if (!reader.exists(resolved)) continue;
|
|
187
190
|
if (!allowAny && !pageIncludeExtensions.includes(path.extname(resolved))) continue;
|
|
188
191
|
return resolved;
|
|
189
192
|
}
|
|
190
|
-
throw
|
|
191
|
-
`Could not resolve include "${spec}" from ${loc}. Looked in site/pages and site/_ — check the path and leading slash
|
|
193
|
+
throw wdError(
|
|
194
|
+
`Could not resolve include "${spec}" from ${loc}. Looked in site/pages and site/_ — check the path and leading slash.`,
|
|
195
|
+
{ code: "WD602", file: fromFile }
|
|
192
196
|
);
|
|
193
197
|
}
|
|
194
198
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// Interpolation primitives: path/value resolution, state-key lookup, scalar
|
|
3
3
|
// parsing, and HTML escaping. These are the leaf utilities the directive
|
|
4
|
-
// handlers, loops, predicates, and markdown layer all build on —
|
|
5
|
-
//
|
|
4
|
+
// handlers, loops, predicates, and markdown layer all build on — it imports
|
|
5
|
+
// only `context.js` (which imports nothing), so it sits at the root of the DAG.
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
+
import { wdError } from "./context.js";
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* @typedef {import("./context.js").Scope} Scope
|
|
10
12
|
* @typedef {import("./context.js").Ctx} Ctx
|
|
@@ -66,7 +68,11 @@ export function getPath(value, segments) {
|
|
|
66
68
|
export function validatePath(path, ctx, use) {
|
|
67
69
|
const segs = path.split(".");
|
|
68
70
|
if (segs.some((seg) => ["constructor", "prototype", "__proto__"].includes(seg))) {
|
|
69
|
-
throw
|
|
71
|
+
throw wdError(`Path segment in "${path}" is not allowed in ${ctx.file}. Use: ${use}`, {
|
|
72
|
+
code: "WD002",
|
|
73
|
+
file: ctx.file,
|
|
74
|
+
hint: use
|
|
75
|
+
});
|
|
70
76
|
}
|
|
71
77
|
return segs;
|
|
72
78
|
}
|
|
@@ -98,8 +104,9 @@ export function interpolateLeaf(value, expr, ctx) {
|
|
|
98
104
|
if (value === null || value === undefined) return "";
|
|
99
105
|
if (Array.isArray(value)) return value.join(", ");
|
|
100
106
|
if (typeof value === "object") {
|
|
101
|
-
throw
|
|
102
|
-
`Cannot interpolate the object value "{ ${expr} }" in ${ctx.file}. Interpolate a field instead — e.g. { ${expr}.title } — or iterate it with @loop ${expr} into item
|
|
107
|
+
throw wdError(
|
|
108
|
+
`Cannot interpolate the object value "{ ${expr} }" in ${ctx.file}. Interpolate a field instead — e.g. { ${expr}.title } — or iterate it with @loop ${expr} into item.`,
|
|
109
|
+
{ code: "WD003", file: ctx.file }
|
|
103
110
|
);
|
|
104
111
|
}
|
|
105
112
|
return String(value);
|
|
@@ -143,11 +150,12 @@ export function parseStateValue(raw, where) {
|
|
|
143
150
|
const open = value[0];
|
|
144
151
|
const close = open === "[" ? "]" : "}";
|
|
145
152
|
const shown = value.length > 60 ? `${value.slice(0, 57)}…` : value;
|
|
146
|
-
throw
|
|
153
|
+
throw wdError(
|
|
147
154
|
`Unbalanced JSON value "${shown}"${where ? ` in ${where}` : ""}: this opens "${open}" but ` +
|
|
148
155
|
`never closes it. :state/:store/:theme accept a multi-line array/object, but the literal ` +
|
|
149
156
|
`must balance (a "${close}" is missing) with no blank lines inside it. ` +
|
|
150
|
-
`Quote it for literal bracket text (e.g. = "[draft]")
|
|
157
|
+
`Quote it for literal bracket text (e.g. = "[draft]").`,
|
|
158
|
+
{ code: "WD004" }
|
|
151
159
|
);
|
|
152
160
|
}
|
|
153
161
|
return value;
|