@zvndev/darkmown 2.8.0 → 2.10.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/AGENTS.md +21 -10
- package/CHANGELOG.md +17 -0
- package/README.md +117 -16
- package/docs/spec-alignment.md +10 -0
- package/grammar/wd-directives.gbnf +1 -1
- package/package.json +11 -3
- package/site/pages/benchmarks/index.skin +67 -0
- package/site/pages/benchmarks/index.wd +126 -0
- package/site/pages/docs/index.wd +48 -3
- package/site/pages/index.wd +1 -0
- package/src/builder.js +140 -20
- package/src/catalog.js +13 -4
- package/src/cli.js +11 -1
- package/src/compiler/body.js +30 -1
- package/src/compiler/context.js +19 -1
- package/src/compiler/includes.js +44 -1
- package/src/compiler/markdown.js +35 -12
- package/src/compiler/media.js +25 -0
- package/src/compiler/page.js +25 -4
- package/src/compiler/structure.js +272 -4
- package/src/compiler.js +13 -3
- package/src/errors.js +24 -0
- package/src/font-subset-worker.js +131 -0
- package/src/font-subset.js +457 -0
- package/src/grammar.js +3 -2
- package/src/headers.js +28 -1
- package/src/image-variant-resize.js +71 -0
- package/src/image-variants.js +294 -0
- package/src/statics.js +22 -14
- package/types/builder.d.ts +34 -1
- package/types/catalog.d.ts +3 -2
- package/types/compiler/context.d.ts +41 -0
- package/types/compiler/includes.d.ts +23 -0
- package/types/compiler/page.d.ts +9 -3
- package/types/compiler/structure.d.ts +53 -1
- package/types/compiler.d.ts +5 -2
- package/types/font-subset.d.ts +140 -0
- package/types/headers.d.ts +14 -0
- package/types/image-variants.d.ts +131 -0
package/src/cli.js
CHANGED
|
@@ -604,7 +604,7 @@ Authoring:
|
|
|
604
604
|
* An incremental (dev `--changed`) build reports what it actually rebuilt —
|
|
605
605
|
* `Rebuilt 2 of 14 routes (/blog/, /blog/hello/) into dist` — naming the routes
|
|
606
606
|
* when there are few enough to read.
|
|
607
|
-
* @param {{ routes: unknown[], feeds: { sitemap: number | null, rss: number | null }, incremental?: { rebuilt: string[], total: number } }} result
|
|
607
|
+
* @param {{ routes: unknown[], feeds: { sitemap: number | null, rss: number | null }, incremental?: { rebuilt: string[], total: number }, images?: { generated: number, cached: number, unavailable: boolean }, fonts?: { subsetted: number, cached: number, unavailable: boolean } }} result
|
|
608
608
|
* @param {string} relativeRoot `dist` relative to cwd, for the message tail.
|
|
609
609
|
* @returns {string}
|
|
610
610
|
*/
|
|
@@ -617,6 +617,16 @@ export function buildSummary(result, relativeRoot) {
|
|
|
617
617
|
const parts = [`Built ${result.routes.length} routes`];
|
|
618
618
|
if (result.feeds.sitemap !== null) parts.push(`sitemap (${result.feeds.sitemap} urls)`);
|
|
619
619
|
if (result.feeds.rss !== null) parts.push(`rss (${result.feeds.rss} posts)`);
|
|
620
|
+
// Image pre-pass (2.9.0): worth reporting only when variants materialized.
|
|
621
|
+
if (result.images && result.images.generated + result.images.cached > 0) {
|
|
622
|
+
parts.push(
|
|
623
|
+
`image variants (${result.images.generated} generated, ${result.images.cached} cached)`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
// Font post-pass (2.10.0): same rule — report only when subsets materialized.
|
|
627
|
+
if (result.fonts && result.fonts.subsetted + result.fonts.cached > 0) {
|
|
628
|
+
parts.push(`fonts (${result.fonts.subsetted} subsetted, ${result.fonts.cached} cached)`);
|
|
629
|
+
}
|
|
620
630
|
return `${parts.join(", ")} into ${relativeRoot}`;
|
|
621
631
|
}
|
|
622
632
|
|
package/src/compiler/body.js
CHANGED
|
@@ -33,8 +33,10 @@ import {
|
|
|
33
33
|
handleTheme,
|
|
34
34
|
renderDemoDirective
|
|
35
35
|
} from "./directives.js";
|
|
36
|
+
import { CHILDREN_SLOT_LINE, childrenSlot } from "./includes.js";
|
|
36
37
|
import { handleLoop, spliceTables } from "./loops.js";
|
|
37
38
|
import { renderProse } from "./markdown.js";
|
|
39
|
+
import { scanIncludeBlock, strayEndinclude } from "./structure.js";
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* @typedef {import("./context.js").Ctx} Ctx
|
|
@@ -148,6 +150,21 @@ export function compileBody(lines, ctx) {
|
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
// A standalone `{ children }` line is an include's slot marker: when the
|
|
154
|
+
// file being compiled was called as a block-form @include, the compiled
|
|
155
|
+
// children HTML (built in the caller's context by handleInclude) splices
|
|
156
|
+
// in here. With no slot in scope — a top-level page — the line falls
|
|
157
|
+
// through to prose and stays the literal text `{ children }`; an include
|
|
158
|
+
// called without a block carries an empty slot and splices nothing.
|
|
159
|
+
if (!inRawBlock && CHILDREN_SLOT_LINE.test(line)) {
|
|
160
|
+
const slot = childrenSlot(ctx);
|
|
161
|
+
if (slot !== null) {
|
|
162
|
+
flush();
|
|
163
|
+
if (slot) push(slot);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
151
168
|
// A directive name ALONE on a line is a mistake with one obvious fix: it is
|
|
152
169
|
// missing its arguments. Every dispatcher below requires a space after the
|
|
153
170
|
// keyword, so such a line used to fall through to prose — and because
|
|
@@ -164,7 +181,15 @@ export function compileBody(lines, ctx) {
|
|
|
164
181
|
|
|
165
182
|
if (/^@include\s/.test(line)) {
|
|
166
183
|
flush();
|
|
167
|
-
|
|
184
|
+
const block = scanIncludeBlock(lines, i);
|
|
185
|
+
if (block) {
|
|
186
|
+
const includeAt = ctx.comp.symbols.length;
|
|
187
|
+
push(handleInclude(line, ctx, i, block.body));
|
|
188
|
+
stampBlockEnd(ctx, includeAt, block.end);
|
|
189
|
+
i = block.end;
|
|
190
|
+
} else {
|
|
191
|
+
push(handleInclude(line, ctx, i));
|
|
192
|
+
}
|
|
168
193
|
continue;
|
|
169
194
|
}
|
|
170
195
|
if (/^@loop\s/.test(line)) {
|
|
@@ -340,6 +365,10 @@ export function compileBody(lines, ctx) {
|
|
|
340
365
|
{ code: "WD009", file: ctx.file, hint: "@loop items into item ... @endloop" }
|
|
341
366
|
);
|
|
342
367
|
}
|
|
368
|
+
// `@endinclude` that reached the dispatcher: every opener that could pair
|
|
369
|
+
// with it already did (scanIncludeBlock consumes paired closers), so this
|
|
370
|
+
// one has no opener of its own — one obvious fix, its own WD6xx code.
|
|
371
|
+
if (/^@endinclude(?:\s|$)/.test(line)) strayEndinclude(line, ctx, i);
|
|
343
372
|
// `:endcarousel` joins the family: a closer with no opener is a mistake with
|
|
344
373
|
// one obvious fix, and the vague "matches none" warning it used to get named
|
|
345
374
|
// the wrong problem ("check the spelling") for a token spelled correctly.
|
package/src/compiler/context.js
CHANGED
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
* that opted into scoping (`scoped` first line). The builder emits these with
|
|
40
40
|
* the scope attribute and runs the unused-selector warning against the stamped
|
|
41
41
|
* subtree; the HTML stamp uses them to scope the matching page/include subtree.
|
|
42
|
+
* @property {Set<string>} embedHosts Frame sources (origins like
|
|
43
|
+
* `https://app.example.com`, or `'self'` for a same-origin embed) of every
|
|
44
|
+
* `:embed` on the page, collected AFTER the YouTube/Vimeo rewrite. The builder
|
|
45
|
+
* records them on the route manifest entry so the per-route CSP's `frame-src`
|
|
46
|
+
* authorizes exactly the hosts this page frames (see `routeCsp` in
|
|
47
|
+
* src/headers.js).
|
|
42
48
|
*/
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -81,6 +87,11 @@
|
|
|
81
87
|
* read threads through — the real filesystem by default, or an in-memory map
|
|
82
88
|
* for `compileFromMemory`. Set once by `createCompilation` and reached via
|
|
83
89
|
* `ctx.comp.reader` in the directive handlers (includes, loop data).
|
|
90
|
+
* @property {Map<string, number[]> | null} imageVariants Build-time-GENERATED
|
|
91
|
+
* responsive variants (2.9.0): the original image's absolute source path →
|
|
92
|
+
* the sorted `@<width>` sizes the builder's pre-pass resized into dist. Null
|
|
93
|
+
* outside a builder-driven compile (e.g. `compileFromMemory`), where the
|
|
94
|
+
* srcset wiring probes author-provided variants only.
|
|
84
95
|
* @property {Symbol[]} symbols Every declared thing this compile saw, in source
|
|
85
96
|
* order, recorded by {@link recordSymbol} from the handler that parsed it.
|
|
86
97
|
* Surfaced on {@link CompiledPage} so a tool layer can answer "what is in this
|
|
@@ -142,6 +153,11 @@
|
|
|
142
153
|
* opener's `file:line` + raw text, set when descending into a loop body so the
|
|
143
154
|
* reactive-nesting-depth error can point at the offending opener.
|
|
144
155
|
* @property {boolean} [loopMeta] Inside a loop body, so `$index`/`$first`/… are valid.
|
|
156
|
+
* @property {number} [includeBlockDepth] Enclosing block-form `@include`
|
|
157
|
+
* CHILDREN levels (0 at page level, absent on a fresh file compile). Set
|
|
158
|
+
* while compiling a block's children so a block include nested past one
|
|
159
|
+
* level inside children is a compile error (WD616, together with the
|
|
160
|
+
* include-stack depth check for include bodies).
|
|
145
161
|
* @property {import("markdown-it").default} [md] Markdown-it instance selected for this file.
|
|
146
162
|
* @property {(lines: string[], ctx: Ctx) => string} compileBody The body
|
|
147
163
|
* dispatcher, threaded through the context so block handlers (`:if`, `:::`,
|
|
@@ -290,6 +306,7 @@ export const LOOP_META = Object.assign(Object.create(null), {
|
|
|
290
306
|
export function createCompilation(reader) {
|
|
291
307
|
return {
|
|
292
308
|
reader,
|
|
309
|
+
imageVariants: null,
|
|
293
310
|
assets: {
|
|
294
311
|
skins: new Set(),
|
|
295
312
|
scripts: new Set(),
|
|
@@ -297,7 +314,8 @@ export function createCompilation(reader) {
|
|
|
297
314
|
runtime: false,
|
|
298
315
|
behaviors: new Set(),
|
|
299
316
|
hasCode: false,
|
|
300
|
-
scopedSkins: new Set()
|
|
317
|
+
scopedSkins: new Set(),
|
|
318
|
+
embedHosts: new Set()
|
|
301
319
|
},
|
|
302
320
|
state: new Map(),
|
|
303
321
|
stores: new Set(),
|
package/src/compiler/includes.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { scopeIdFor } from "../skin.js";
|
|
10
10
|
import { pageIncludeExtensions, wdError } from "./context.js";
|
|
11
|
-
import { stripQuotes } from "./interpolation.js";
|
|
11
|
+
import { lookupVar, stripQuotes } from "./interpolation.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @typedef {import("./context.js").Paths} Paths
|
|
@@ -244,6 +244,49 @@ export function isAllowedInclude(file, context, reader) {
|
|
|
244
244
|
return contains(canonical(path.resolve(file)), roots.map(canonical));
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// The `{ children }` slot (block-form @include).
|
|
249
|
+
//
|
|
250
|
+
// `@include /card.wd … @endinclude` compiles the lines between opener and
|
|
251
|
+
// closer in the CALLER's context and hands the resulting HTML to the include
|
|
252
|
+
// file, which places it with a standalone `{ children }` line. The hand-off
|
|
253
|
+
// travels in the static scope chain under a key no author syntax can produce
|
|
254
|
+
// (interpolation, `with` args, and `:state` names are all `[A-Za-z0-9_$-]`
|
|
255
|
+
// shaped), so a `{ children }` interpolation in prose never resolves it — only
|
|
256
|
+
// the body parser's slot-marker intercept reads it. Pure macro expansion: the
|
|
257
|
+
// children compile once, at build time, and the include stays zero-JS unless
|
|
258
|
+
// the children themselves bind state.
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Scope key carrying the compiled children HTML for the include currently
|
|
263
|
+
* being compiled. Control-character-prefixed so no author-visible name can
|
|
264
|
+
* collide with it or read it through `{ }` interpolation.
|
|
265
|
+
*/
|
|
266
|
+
export const INCLUDE_CHILDREN = "\0children";
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* A line that is exactly the slot marker (`{ children }`, whitespace-tolerant),
|
|
270
|
+
* matching the spelling freedom of `{ name }` interpolation. Shared by the
|
|
271
|
+
* body parser (which splices the slot) and the include handler (which scans an
|
|
272
|
+
* include's body for whether the slot is ever used).
|
|
273
|
+
* @type {RegExp}
|
|
274
|
+
*/
|
|
275
|
+
export const CHILDREN_SLOT_LINE = /^\s*\{\s*children\s*\}\s*$/;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The compiled children HTML for the include currently compiling, or null when
|
|
279
|
+
* the file is not being compiled as an include at all (a top-level page) — in
|
|
280
|
+
* which case a `{ children }` line stays literal text. An include called
|
|
281
|
+
* WITHOUT a block carries "" (the slot renders empty).
|
|
282
|
+
* @param {import("./context.js").Ctx} ctx
|
|
283
|
+
* @returns {string | null}
|
|
284
|
+
*/
|
|
285
|
+
export function childrenSlot(ctx) {
|
|
286
|
+
const found = lookupVar(ctx.scope, INCLUDE_CHILDREN);
|
|
287
|
+
return found.found ? /** @type {string} */ (found.value) : null;
|
|
288
|
+
}
|
|
289
|
+
|
|
247
290
|
// ---------------------------------------------------------------------------
|
|
248
291
|
// Plain .md hints
|
|
249
292
|
// ---------------------------------------------------------------------------
|
package/src/compiler/markdown.js
CHANGED
|
@@ -363,25 +363,36 @@ function attrBindPlugin(mdInstance) {
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
// ---------------------------------------------------------------------------
|
|
366
|
-
// RESPONSIVE IMAGE VARIANTS (the `@width` convention, 2.8.0 / A2
|
|
366
|
+
// RESPONSIVE IMAGE VARIANTS (the `@width` convention, 2.8.0 / A2; generated
|
|
367
|
+
// variants, 2.9.0).
|
|
367
368
|
//
|
|
368
|
-
//
|
|
369
|
-
// convention wires variants the AUTHOR (or their CDN tooling) already made:
|
|
369
|
+
// Variants come from two places, and this wiring treats them identically:
|
|
370
370
|
// next to `/doctors/dr-x.webp` sit `dr-x@640.webp`, `dr-x@1280.webp` — same
|
|
371
371
|
// directory, same extension, `@<width>` before it. When at least one exists,
|
|
372
372
|
// the markdown image gains a `srcset` (variants ascending, then the original
|
|
373
373
|
// at its intrinsic width) so the browser downloads the size it needs instead
|
|
374
374
|
// of the full-resolution file. The original stays the `src` fallback.
|
|
375
375
|
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
376
|
+
// * AUTHOR-PROVIDED (2.8.0): files the author (or their CDN tooling) placed
|
|
377
|
+
// beside the source. Discovery is an existence PROBE over
|
|
378
|
+
// {@link VARIANT_WIDTHS} through the injected reader: the Reader contract
|
|
379
|
+
// has no directory listing, so an off-list width (say `@500`) is simply not
|
|
380
|
+
// wired — a deliberate, documented miss, never an error. Each variant's
|
|
381
|
+
// descriptor width is its REAL intrinsic width from the image header (a
|
|
382
|
+
// `@640` file that actually holds 600px says 600w); when the bytes cannot
|
|
383
|
+
// be read or parsed (the in-memory reader has no binaries), the filename
|
|
384
|
+
// width is the fallback.
|
|
385
|
+
// * BUILD-TIME GENERATED (2.9.0): the builder's pre-pass (src/image-variants.js)
|
|
386
|
+
// resized the source with the optional sharp dependency and wrote the
|
|
387
|
+
// variants into dist. It hands this compile a map (original's absolute
|
|
388
|
+
// source path → widths) via `comp.imageVariants`; the widths ARE the
|
|
389
|
+
// descriptor widths because the generator made them so. An author-provided
|
|
390
|
+
// file at the same width always wins — the generator skips those widths,
|
|
391
|
+
// and the href dedupe below keeps it that way even against a stale map.
|
|
392
|
+
//
|
|
393
|
+
// The original's intrinsic width follows the same measure-with-fallback rule,
|
|
394
|
+
// except it has no filename fallback: unreadable means it is left out of the
|
|
395
|
+
// srcset and stays the plain `src` default.
|
|
385
396
|
//
|
|
386
397
|
// A bound src (``) is repainted by the runtime, so a srcset
|
|
387
398
|
// computed from the seed would go stale on the first render — those images are
|
|
@@ -483,6 +494,18 @@ function buildSrcset(src, ctx) {
|
|
|
483
494
|
if (!reader.exists(abs)) continue;
|
|
484
495
|
candidates.push({ href: `${base}@${width}${ext}`, width: measureWidth(abs, reader) ?? width });
|
|
485
496
|
}
|
|
497
|
+
// Builder-generated variants (2.9.0): resized into dist by the pre-pass, so
|
|
498
|
+
// the reader cannot probe them — they arrive through the compile instead.
|
|
499
|
+
// An author-provided sibling at the same width wins (the generator skips
|
|
500
|
+
// those widths; the href check keeps precedence even against a stale map).
|
|
501
|
+
const generated = ctx.comp.imageVariants?.get(path.join(root, `${relBase}${ext}`));
|
|
502
|
+
if (generated) {
|
|
503
|
+
for (const width of generated) {
|
|
504
|
+
const href = `${base}@${width}${ext}`;
|
|
505
|
+
if (candidates.some((c) => c.href === href)) continue;
|
|
506
|
+
candidates.push({ href, width });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
486
509
|
if (!candidates.length) return null;
|
|
487
510
|
const intrinsic = measureWidth(path.join(root, `${relBase}${ext}`), reader);
|
|
488
511
|
if (intrinsic) candidates.push({ href: src, width: intrinsic });
|
package/src/compiler/media.js
CHANGED
|
@@ -175,6 +175,7 @@ export function handleEmbed(line, ctx, index) {
|
|
|
175
175
|
const sizing = embedSizing(attrs, ctx, index);
|
|
176
176
|
const allow = embedAllow(attrs, ctx, index);
|
|
177
177
|
const { url, label } = resolveEmbed(raw, ctx);
|
|
178
|
+
recordEmbedHost(ctx, url);
|
|
178
179
|
const title = attrs.get("title") || "";
|
|
179
180
|
const iframe =
|
|
180
181
|
`<iframe src="${escapeHtml(url)}" title="${escapeHtml(title || label)}" loading="lazy" ` +
|
|
@@ -237,6 +238,30 @@ function embedAllow(attrs, ctx, index) {
|
|
|
237
238
|
return tokens.join("; ");
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Record the frame source the emitted iframe actually loads, so the builder can
|
|
243
|
+
* union it into this route's CSP `frame-src`. An absolute http(s) URL contributes
|
|
244
|
+
* its origin; a relative URL frames the page's own origin, so it contributes
|
|
245
|
+
* `'self'`. A `{ state }`-driven URL is unknowable at compile time and records
|
|
246
|
+
* nothing (widening that policy is the app's deploy-config decision, exactly
|
|
247
|
+
* like a remote `:fetch` target widening `connect-src`).
|
|
248
|
+
* @param {Ctx} ctx
|
|
249
|
+
* @param {string} url The FINAL iframe src (post YouTube/Vimeo rewrite).
|
|
250
|
+
* @returns {void}
|
|
251
|
+
*/
|
|
252
|
+
function recordEmbedHost(ctx, url) {
|
|
253
|
+
if (url.startsWith("{")) return;
|
|
254
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
255
|
+
ctx.comp.assets.embedHosts.add("'self'");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
ctx.comp.assets.embedHosts.add(new URL(url).origin);
|
|
260
|
+
} catch {
|
|
261
|
+
/* a malformed-but-scheme-valid URL paints as-is; nothing to authorize */
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
240
265
|
/**
|
|
241
266
|
* Map a watch/share URL to its embeddable form. YouTube → no-cookie embed, Vimeo →
|
|
242
267
|
* player; anything else is validated as a generic http(s) iframe source.
|
package/src/compiler/page.js
CHANGED
|
@@ -14,6 +14,7 @@ import { parseFrontmatter, resolveSharedSkins, warnLikelyFrontmatter } from "./f
|
|
|
14
14
|
import { imageSize } from "./image-size.js";
|
|
15
15
|
import {
|
|
16
16
|
collectColocatedAssets,
|
|
17
|
+
isAllowedInclude,
|
|
17
18
|
scanMarkdownHints,
|
|
18
19
|
scopedSkinFor,
|
|
19
20
|
scopeInfoForSkin,
|
|
@@ -45,7 +46,7 @@ const SKIP_LINK_STYLE =
|
|
|
45
46
|
* Compile a page source file into a full HTML document plus its assets.
|
|
46
47
|
* @param {string} file Absolute path to the source `.md`/`.wd` file.
|
|
47
48
|
* @param {Paths} context Resolved project paths.
|
|
48
|
-
* @param {{ feed?: { href: string, title: string }, site?: SiteContext, collections?: Map<string, import("./collections.js").CollectionRow[]>, vars?: Record<string, unknown>, reader?: import("./reader.js").Reader }} [options]
|
|
49
|
+
* @param {{ feed?: { href: string, title: string }, site?: SiteContext, collections?: Map<string, import("./collections.js").CollectionRow[]>, vars?: Record<string, unknown>, reader?: import("./reader.js").Reader, imageVariants?: Map<string, number[]> | null }} [options]
|
|
49
50
|
* When a site-wide RSS feed is emitted (the home page set `site_url` and the
|
|
50
51
|
* site has dated posts), `feed` carries its absolute href + title so every page
|
|
51
52
|
* links it. `site` carries the origin + this page's own route (and its resolved
|
|
@@ -53,14 +54,23 @@ const SKIP_LINK_STYLE =
|
|
|
53
54
|
* build-time collection index a bare-name `@loop` resolves against; `vars` seeds
|
|
54
55
|
* the document scope (the pager `page` object on a paginated route). `reader` is
|
|
55
56
|
* the source reader (fs-backed by default via `src/compiler.js`; in-memory for
|
|
56
|
-
* `compileFromMemory`).
|
|
57
|
+
* `compileFromMemory`). `imageVariants` is the builder's generated-variant map
|
|
58
|
+
* (2.9.0): original absolute source path → `@<width>` sizes resized into dist.
|
|
57
59
|
* @returns {CompiledPage}
|
|
58
60
|
*/
|
|
59
61
|
export function compilePage(file, context, options = {}) {
|
|
60
62
|
// The raw entry point requires a reader — the Node barrel (`src/compiler.js`)
|
|
61
63
|
// injects `fsReader()`, and `compileFromMemory` injects a `memoryReader`.
|
|
62
64
|
const reader = /** @type {import("./reader.js").Reader} */ (options.reader);
|
|
63
|
-
const compiled = compileDocument(
|
|
65
|
+
const compiled = compileDocument(
|
|
66
|
+
file,
|
|
67
|
+
context,
|
|
68
|
+
[],
|
|
69
|
+
options.vars,
|
|
70
|
+
options.collections,
|
|
71
|
+
reader,
|
|
72
|
+
options.imageVariants ?? null
|
|
73
|
+
);
|
|
64
74
|
const title = String(compiled.meta.title || "Darkmown");
|
|
65
75
|
const description = String(compiled.meta.description || "");
|
|
66
76
|
// Per-page document language for the `<html lang>` attribute (`lang: fr` in
|
|
@@ -399,6 +409,10 @@ function srcOf(tag) {
|
|
|
399
409
|
* Read an image's intrinsic dimensions from disk, or null when the src is
|
|
400
410
|
* remote/relative/unreadable. Resolution mirrors the asset emit: `/__wd/media/x`
|
|
401
411
|
* comes from the shelf (`site/_`), other absolute paths from `site/pages`.
|
|
412
|
+
* Containment mirrors the `@include` sandbox ({@link isAllowedInclude}): a src
|
|
413
|
+
* that resolves OUTSIDE those roots — ``, or a symlink
|
|
414
|
+
* pointing out — is treated exactly like a missing image (no dimensions), never
|
|
415
|
+
* read.
|
|
402
416
|
* @param {string} src
|
|
403
417
|
* @param {Paths} paths
|
|
404
418
|
* @param {import("./reader.js").Reader} reader
|
|
@@ -414,6 +428,7 @@ function measureImage(src, paths, reader) {
|
|
|
414
428
|
} else {
|
|
415
429
|
return null; // page-relative: the source directory is lost after assembly
|
|
416
430
|
}
|
|
431
|
+
if (!isAllowedInclude(filePath, paths, reader)) return null;
|
|
417
432
|
try {
|
|
418
433
|
// `imageSize` itself never throws (an unsupported or malformed image is just
|
|
419
434
|
// null); the try/catch is for the read, which does throw on a missing file.
|
|
@@ -434,6 +449,10 @@ function measureImage(src, paths, reader) {
|
|
|
434
449
|
* Build-time collection index a bare-name `@loop` resolves against.
|
|
435
450
|
* @param {import("./reader.js").Reader} [reader] Source reader (fs-backed when
|
|
436
451
|
* omitted via `src/compiler.js`; in-memory for `compileFromMemory`).
|
|
452
|
+
* @param {Map<string, number[]> | null} [imageVariants] Build-time-generated
|
|
453
|
+
* responsive variants (original's absolute source path → widths), threaded by
|
|
454
|
+
* the builder so the srcset wiring includes them alongside author-provided
|
|
455
|
+
* `@<width>` siblings.
|
|
437
456
|
* @returns {CompiledDocument}
|
|
438
457
|
*/
|
|
439
458
|
export function compileDocument(
|
|
@@ -442,13 +461,15 @@ export function compileDocument(
|
|
|
442
461
|
stack = [],
|
|
443
462
|
vars = {},
|
|
444
463
|
collections = new Map(),
|
|
445
|
-
reader
|
|
464
|
+
reader,
|
|
465
|
+
imageVariants = null
|
|
446
466
|
) {
|
|
447
467
|
// reader is injected by every caller (the Node barrel's `fsReader()` or
|
|
448
468
|
// `compileFromMemory`'s `memoryReader`); it is only trailing-optional here so
|
|
449
469
|
// the defaulted `vars`/`collections` params can precede it.
|
|
450
470
|
const comp = createCompilation(/** @type {import("./reader.js").Reader} */ (reader));
|
|
451
471
|
comp.collections = collections;
|
|
472
|
+
comp.imageVariants = imageVariants;
|
|
452
473
|
const result = compileFile(
|
|
453
474
|
file,
|
|
454
475
|
context,
|