@zvndev/darkmown 2.9.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 +20 -9
- package/CHANGELOG.md +13 -0
- package/README.md +104 -5
- package/docs/spec-alignment.md +7 -1
- package/grammar/wd-directives.gbnf +1 -1
- package/package.json +7 -3
- package/site/pages/benchmarks/index.skin +67 -0
- package/site/pages/benchmarks/index.wd +126 -0
- package/site/pages/docs/index.wd +47 -2
- package/site/pages/index.wd +1 -0
- package/src/builder.js +66 -13
- package/src/catalog.js +13 -4
- package/src/cli.js +5 -1
- package/src/compiler/body.js +30 -1
- package/src/compiler/context.js +13 -1
- package/src/compiler/includes.js +44 -1
- package/src/compiler/media.js +25 -0
- package/src/compiler/page.js +6 -0
- package/src/compiler/structure.js +272 -4
- 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/statics.js +22 -14
- package/types/builder.d.ts +25 -1
- package/types/catalog.d.ts +3 -2
- package/types/compiler/context.d.ts +28 -0
- package/types/compiler/includes.d.ts +23 -0
- package/types/compiler/structure.d.ts +53 -1
- package/types/font-subset.d.ts +140 -0
- package/types/headers.d.ts +14 -0
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
* that opted into scoping (`scoped` first line). The builder emits these with
|
|
27
27
|
* the scope attribute and runs the unused-selector warning against the stamped
|
|
28
28
|
* subtree; the HTML stamp uses them to scope the matching page/include subtree.
|
|
29
|
+
* @property {Set<string>} embedHosts Frame sources (origins like
|
|
30
|
+
* `https://app.example.com`, or `'self'` for a same-origin embed) of every
|
|
31
|
+
* `:embed` on the page, collected AFTER the YouTube/Vimeo rewrite. The builder
|
|
32
|
+
* records them on the route manifest entry so the per-route CSP's `frame-src`
|
|
33
|
+
* authorizes exactly the hosts this page frames (see `routeCsp` in
|
|
34
|
+
* src/headers.js).
|
|
29
35
|
*/
|
|
30
36
|
/**
|
|
31
37
|
* The pagination intent a `@loop … paginate N` records during compile, so the
|
|
@@ -129,6 +135,11 @@
|
|
|
129
135
|
* opener's `file:line` + raw text, set when descending into a loop body so the
|
|
130
136
|
* reactive-nesting-depth error can point at the offending opener.
|
|
131
137
|
* @property {boolean} [loopMeta] Inside a loop body, so `$index`/`$first`/… are valid.
|
|
138
|
+
* @property {number} [includeBlockDepth] Enclosing block-form `@include`
|
|
139
|
+
* CHILDREN levels (0 at page level, absent on a fresh file compile). Set
|
|
140
|
+
* while compiling a block's children so a block include nested past one
|
|
141
|
+
* level inside children is a compile error (WD616, together with the
|
|
142
|
+
* include-stack depth check for include bodies).
|
|
132
143
|
* @property {import("markdown-it").default} [md] Markdown-it instance selected for this file.
|
|
133
144
|
* @property {(lines: string[], ctx: Ctx) => string} compileBody The body
|
|
134
145
|
* dispatcher, threaded through the context so block handlers (`:if`, `:::`,
|
|
@@ -410,6 +421,15 @@ export type Assets = {
|
|
|
410
421
|
* subtree; the HTML stamp uses them to scope the matching page/include subtree.
|
|
411
422
|
*/
|
|
412
423
|
scopedSkins: Set<string>;
|
|
424
|
+
/**
|
|
425
|
+
* Frame sources (origins like
|
|
426
|
+
* `https://app.example.com`, or `'self'` for a same-origin embed) of every
|
|
427
|
+
* `:embed` on the page, collected AFTER the YouTube/Vimeo rewrite. The builder
|
|
428
|
+
* records them on the route manifest entry so the per-route CSP's `frame-src`
|
|
429
|
+
* authorizes exactly the hosts this page frames (see `routeCsp` in
|
|
430
|
+
* src/headers.js).
|
|
431
|
+
*/
|
|
432
|
+
embedHosts: Set<string>;
|
|
413
433
|
};
|
|
414
434
|
/**
|
|
415
435
|
* The pagination intent a `@loop … paginate N` records during compile, so the
|
|
@@ -630,6 +650,14 @@ export type Ctx = {
|
|
|
630
650
|
* Inside a loop body, so `$index`/`$first`/… are valid.
|
|
631
651
|
*/
|
|
632
652
|
loopMeta?: boolean | undefined;
|
|
653
|
+
/**
|
|
654
|
+
* Enclosing block-form `@include`
|
|
655
|
+
* CHILDREN levels (0 at page level, absent on a fresh file compile). Set
|
|
656
|
+
* while compiling a block's children so a block include nested past one
|
|
657
|
+
* level inside children is a compile error (WD616, together with the
|
|
658
|
+
* include-stack depth check for include bodies).
|
|
659
|
+
*/
|
|
660
|
+
includeBlockDepth?: number | undefined;
|
|
633
661
|
/**
|
|
634
662
|
* Markdown-it instance selected for this file.
|
|
635
663
|
*/
|
|
@@ -101,6 +101,15 @@ export function resolveInclude(spec: string, fromFile: string, context: Paths, a
|
|
|
101
101
|
* @returns {boolean}
|
|
102
102
|
*/
|
|
103
103
|
export function isAllowedInclude(file: string, context: Paths, reader?: import("./reader.js").Reader): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The compiled children HTML for the include currently compiling, or null when
|
|
106
|
+
* the file is not being compiled as an include at all (a top-level page) — in
|
|
107
|
+
* which case a `{ children }` line stays literal text. An include called
|
|
108
|
+
* WITHOUT a block carries "" (the slot renders empty).
|
|
109
|
+
* @param {import("./context.js").Ctx} ctx
|
|
110
|
+
* @returns {string | null}
|
|
111
|
+
*/
|
|
112
|
+
export function childrenSlot(ctx: import("./context.js").Ctx): string | null;
|
|
104
113
|
/**
|
|
105
114
|
* Warn when a plain `.md` file contains `.wd`-only syntax that stays inert.
|
|
106
115
|
* @param {string} body
|
|
@@ -109,6 +118,20 @@ export function isAllowedInclude(file: string, context: Paths, reader?: import("
|
|
|
109
118
|
* @returns {void}
|
|
110
119
|
*/
|
|
111
120
|
export function scanMarkdownHints(body: string, file: string, comp: Compilation): void;
|
|
121
|
+
/**
|
|
122
|
+
* Scope key carrying the compiled children HTML for the include currently
|
|
123
|
+
* being compiled. Control-character-prefixed so no author-visible name can
|
|
124
|
+
* collide with it or read it through `{ }` interpolation.
|
|
125
|
+
*/
|
|
126
|
+
export const INCLUDE_CHILDREN: "\0children";
|
|
127
|
+
/**
|
|
128
|
+
* A line that is exactly the slot marker (`{ children }`, whitespace-tolerant),
|
|
129
|
+
* matching the spelling freedom of `{ name }` interpolation. Shared by the
|
|
130
|
+
* body parser (which splices the slot) and the include handler (which scans an
|
|
131
|
+
* include's body for whether the slot is ever used).
|
|
132
|
+
* @type {RegExp}
|
|
133
|
+
*/
|
|
134
|
+
export const CHILDREN_SLOT_LINE: RegExp;
|
|
112
135
|
export type Paths = import("./context.js").Paths;
|
|
113
136
|
export type Assets = import("./context.js").Assets;
|
|
114
137
|
export type Compilation = import("./context.js").Compilation;
|
|
@@ -24,12 +24,64 @@ export function takeA11yAttrs(rest: string, ctx: Ctx, index: number, what: strin
|
|
|
24
24
|
*/
|
|
25
25
|
export function a11yAttrHtml(attrs: [string, string][]): string;
|
|
26
26
|
/**
|
|
27
|
+
* Compile an `@include` line — inline form (`@include /nav.wd`) or block form
|
|
28
|
+
* (`@include /card.wd … @endinclude`, with `childrenLines` holding the lines
|
|
29
|
+
* between opener and closer, captured by {@link scanIncludeBlock}).
|
|
30
|
+
*
|
|
31
|
+
* Block form is compile-time macro expansion and nothing more: the children
|
|
32
|
+
* lines compile in the CALLER's context (its scope, its `:state`, its loop
|
|
33
|
+
* item when the call sits in a `@loop` body) BEFORE the target compiles, and
|
|
34
|
+
* the resulting HTML rides into the include's scope under
|
|
35
|
+
* {@link INCLUDE_CHILDREN}, where the body parser splices it at each
|
|
36
|
+
* `{ children }` slot line. No runtime marker, no reactivity of its own.
|
|
37
|
+
* Children compile ONCE even when the include places the slot twice, so state
|
|
38
|
+
* a child line declares is declared once in the caller.
|
|
39
|
+
*
|
|
40
|
+
* The include file's own frontmatter may declare `defaults: [name="value" …]`
|
|
41
|
+
* fallback values for `with` arguments (see {@link parseIncludeDefaults}); a
|
|
42
|
+
* caller-provided argument always wins over a default, and an argument with
|
|
43
|
+
* neither keeps the literal-braces behavior.
|
|
27
44
|
* @param {string} line
|
|
28
45
|
* @param {Ctx} ctx
|
|
29
46
|
* @param {number} index 0-based line index for `file:line` errors.
|
|
47
|
+
* @param {string[]} [childrenLines] Lines between `@include` and `@endinclude`
|
|
48
|
+
* for the block form; undefined for the inline form.
|
|
30
49
|
* @returns {string}
|
|
31
50
|
*/
|
|
32
|
-
export function handleInclude(line: string, ctx: Ctx, index: number): string;
|
|
51
|
+
export function handleInclude(line: string, ctx: Ctx, index: number, childrenLines?: string[]): string;
|
|
52
|
+
/**
|
|
53
|
+
* Scan ahead from an `@include` opener for the `@endinclude` that pairs with
|
|
54
|
+
* it. Pairing is nearest-opener-first: a nested `@include` line pushes, an
|
|
55
|
+
* `@endinclude` line pops the most recent unclosed opener, and the first
|
|
56
|
+
* `@endinclude` found with nothing on the stack belongs to THIS opener — so
|
|
57
|
+
* nested blocks pair correctly, and an inline `@include` earlier in the file
|
|
58
|
+
* than a block (the common nav-include-then-card-block page) is unaffected.
|
|
59
|
+
* Fenced code is skipped, exactly as in the other block scanners.
|
|
60
|
+
*
|
|
61
|
+
* Returns null when no closer pairs — the include is the INLINE form, which
|
|
62
|
+
* stays legal (backward compatible): an opener that never pairs is an inline
|
|
63
|
+
* include followed by ordinary prose, and the two are indistinguishable, so
|
|
64
|
+
* there is deliberately no "unclosed block" error.
|
|
65
|
+
* @param {string[]} lines The current body slice.
|
|
66
|
+
* @param {number} start 0-based index of the `@include` opener line.
|
|
67
|
+
* @returns {{ body: string[], end: number } | null} The children lines and the
|
|
68
|
+
* closer's 0-based index, or null for the inline form.
|
|
69
|
+
*/
|
|
70
|
+
export function scanIncludeBlock(lines: string[], start: number): {
|
|
71
|
+
body: string[];
|
|
72
|
+
end: number;
|
|
73
|
+
} | null;
|
|
74
|
+
/**
|
|
75
|
+
* `@endinclude` reached the body dispatcher, meaning no `@include` opener
|
|
76
|
+
* claimed it — the one-closer-too-many case (a typo'd closer, or a nested
|
|
77
|
+
* block attempted inside a children block, where the inner opener already
|
|
78
|
+
* took the closer the author meant for the outer one). Always throws.
|
|
79
|
+
* @param {string} line The offending line, for the message.
|
|
80
|
+
* @param {Ctx} ctx
|
|
81
|
+
* @param {number} index 0-based line index for `file:line`.
|
|
82
|
+
* @returns {never}
|
|
83
|
+
*/
|
|
84
|
+
export function strayEndinclude(line: string, ctx: Ctx, index: number): never;
|
|
33
85
|
/**
|
|
34
86
|
* @param {string} header
|
|
35
87
|
* @param {string[]} bodyLines
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subset every project-local font referenced by the emitted stylesheets and
|
|
3
|
+
* rewrite the dist CSS to lead with the subset woff2.
|
|
4
|
+
*
|
|
5
|
+
* Skips, none of them errors: remote/protocol-relative/data/relative URLs
|
|
6
|
+
* (silently), already-woff2 sources (silently), URLs that escape or miss the
|
|
7
|
+
* project (silently — a broken link is an authoring bug, not a subsetting one).
|
|
8
|
+
* An unreadable font file, and one the toolchain fails on, get a hint naming
|
|
9
|
+
* the file and ship as-is. With no `@font-face` in any emitted stylesheet the
|
|
10
|
+
* pass is a no-op and never even scans the HTML.
|
|
11
|
+
* @param {Paths} paths
|
|
12
|
+
* @param {{ cacheDir?: string, quiet?: boolean,
|
|
13
|
+
* runJobs?: (payload: { charset: number[], jobs: SubsetJob[] }) => SubsetResult }} [options]
|
|
14
|
+
* `quiet` suppresses the hints (dev-server rebuilds). `runJobs` is the subset
|
|
15
|
+
* seam — the default spawns the worker process; tests inject fakes for the
|
|
16
|
+
* unavailable/failure paths.
|
|
17
|
+
* @returns {FontSubsetPlan}
|
|
18
|
+
*/
|
|
19
|
+
export function subsetFonts(paths: Paths, options?: {
|
|
20
|
+
cacheDir?: string;
|
|
21
|
+
quiet?: boolean;
|
|
22
|
+
runJobs?: (payload: {
|
|
23
|
+
charset: number[];
|
|
24
|
+
jobs: SubsetJob[];
|
|
25
|
+
}) => SubsetResult;
|
|
26
|
+
}): FontSubsetPlan;
|
|
27
|
+
/**
|
|
28
|
+
* The default subset seam: run every pending job in ONE child process (the
|
|
29
|
+
* build is synchronous; the wasm API is not), communicating through temp JSON
|
|
30
|
+
* files rather than stdio so a large payload never hits a pipe buffer. The
|
|
31
|
+
* worker always writes a results file — a missing or unparsable one means the
|
|
32
|
+
* child itself died, which is reported as `unavailable` (the build then behaves
|
|
33
|
+
* as if the toolchain were absent: a hint, no subsets, no failure).
|
|
34
|
+
* @param {{ charset: number[], jobs: SubsetJob[] }} payload
|
|
35
|
+
* @param {(cmd: string, args: string[], opts: object) => unknown} [spawn]
|
|
36
|
+
* Injectable for tests; the real one is `spawnSync`.
|
|
37
|
+
* @returns {SubsetResult}
|
|
38
|
+
*/
|
|
39
|
+
export function runSubsetJobs(payload: {
|
|
40
|
+
charset: number[];
|
|
41
|
+
jobs: SubsetJob[];
|
|
42
|
+
}, spawn?: (cmd: string, args: string[], opts: object) => unknown): SubsetResult;
|
|
43
|
+
/**
|
|
44
|
+
* The subset charset for a build: the base set plus every code point appearing
|
|
45
|
+
* in the given texts (the emitted pages' HTML). Numeric character references
|
|
46
|
+
* (`★`, `★`) are decoded first so a character the compiler emitted
|
|
47
|
+
* as an entity still lands in the union. Tags and attribute syntax contribute
|
|
48
|
+
* only ASCII, which the base set already carries, so the whole-file scan can
|
|
49
|
+
* only ever over-include by a handful of glyphs.
|
|
50
|
+
* @param {string[]} texts
|
|
51
|
+
* @returns {number[]} Sorted ascending, deduped.
|
|
52
|
+
*/
|
|
53
|
+
export function charsetFromTexts(texts: string[]): number[];
|
|
54
|
+
/**
|
|
55
|
+
* Rewrite the `@font-face` blocks of an emitted stylesheet so each subsetted
|
|
56
|
+
* font's `src` leads with the subset woff2, the original kept as fallback:
|
|
57
|
+
* src: url("/f/a-subset.woff2") format("woff2"), url("/f/a.ttf") format("truetype");
|
|
58
|
+
* Only the `src` declaration changes — font-display and every other descriptor
|
|
59
|
+
* pass through byte-identically. Idempotent: a block already carrying the
|
|
60
|
+
* subset reference (a previous build's rewrite of a since-unedited skin) is
|
|
61
|
+
* left alone.
|
|
62
|
+
* @param {string} css Emitted stylesheet contents.
|
|
63
|
+
* @param {Map<string, string>} subsetByUrl url() target as written → subset URL.
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function rewriteFontFaceCss(css: string, subsetByUrl: Map<string, string>): string;
|
|
67
|
+
/** The base charset, sorted ascending. */
|
|
68
|
+
export const BASE_CODEPOINTS: number[];
|
|
69
|
+
export type Paths = import("./config.js").Paths;
|
|
70
|
+
/**
|
|
71
|
+
* One subset handed to the worker: reduce `source` to the shared charset and
|
|
72
|
+
* write the woff2 result to `out`, the cache path.
|
|
73
|
+
*/
|
|
74
|
+
export type SubsetJob = {
|
|
75
|
+
/**
|
|
76
|
+
* Absolute path of the original font file.
|
|
77
|
+
*/
|
|
78
|
+
source: string;
|
|
79
|
+
/**
|
|
80
|
+
* Absolute cache path the subset woff2 is written to.
|
|
81
|
+
*/
|
|
82
|
+
out: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* What the worker reports back. `unavailable` means the toolchain could not be
|
|
86
|
+
* loaded at all (harfbuzzjs/wawoff2 not installed) — every pending font then
|
|
87
|
+
* ships unsubsetted. `failed` carries one entry per font the toolchain itself
|
|
88
|
+
* rejected (corrupt payload, unsupported format).
|
|
89
|
+
*/
|
|
90
|
+
export type SubsetResult = {
|
|
91
|
+
unavailable: boolean;
|
|
92
|
+
failed: {
|
|
93
|
+
file: string;
|
|
94
|
+
error: string;
|
|
95
|
+
}[];
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The post-pass result, feeding the build summary. `unavailable` marks the
|
|
99
|
+
* toolchain-absent build (which is otherwise identical to not having the
|
|
100
|
+
* feature).
|
|
101
|
+
*/
|
|
102
|
+
export type FontSubsetPlan = {
|
|
103
|
+
/**
|
|
104
|
+
* Fonts freshly subsetted this build.
|
|
105
|
+
*/
|
|
106
|
+
subsetted: number;
|
|
107
|
+
/**
|
|
108
|
+
* Fonts served from the on-disk cache.
|
|
109
|
+
*/
|
|
110
|
+
cached: number;
|
|
111
|
+
/**
|
|
112
|
+
* harfbuzzjs/wawoff2 could not be loaded.
|
|
113
|
+
*/
|
|
114
|
+
unavailable: boolean;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* A project-local font referenced by an emitted `@font-face` rule.
|
|
118
|
+
*/
|
|
119
|
+
export type FontRef = {
|
|
120
|
+
/**
|
|
121
|
+
* The url() target exactly as written in the CSS.
|
|
122
|
+
*/
|
|
123
|
+
url: string;
|
|
124
|
+
/**
|
|
125
|
+
* Absolute path of the font file it resolves to.
|
|
126
|
+
*/
|
|
127
|
+
source: string;
|
|
128
|
+
/**
|
|
129
|
+
* Served URL of the subset (`<stem>-subset.woff2`).
|
|
130
|
+
*/
|
|
131
|
+
subsetUrl: string;
|
|
132
|
+
/**
|
|
133
|
+
* Absolute dist path the subset is written to.
|
|
134
|
+
*/
|
|
135
|
+
subsetDist: string;
|
|
136
|
+
/**
|
|
137
|
+
* Absolute cache path, set once the hashes are known.
|
|
138
|
+
*/
|
|
139
|
+
cacheFile?: string | undefined;
|
|
140
|
+
};
|
package/types/headers.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSP for one route: its base policy (`REACTIVE_CSP`/`STATIC_CSP`) with the
|
|
3
|
+
* page's `:embed` frame hosts unioned into `frame-src`. The compiler records the
|
|
4
|
+
* FINAL iframe src of every `:embed` (after the YouTube/Vimeo rewrite), so a
|
|
5
|
+
* generic embed — a booking widget, a map — is authorized on exactly the pages
|
|
6
|
+
* that carry it instead of being blocked by the shipped policy. YouTube/Vimeo
|
|
7
|
+
* embeds dedupe against the base list, so a page whose embeds are all covered
|
|
8
|
+
* already produces a byte-identical policy, as does a page with no embeds.
|
|
9
|
+
* @param {boolean} reactive Whether the route ships the reactive runtime.
|
|
10
|
+
* @param {Iterable<string>} [embedHosts] Origins (`https://app.example.com`) or
|
|
11
|
+
* `'self'` for a same-origin embed, collected at compile time.
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function routeCsp(reactive: boolean, embedHosts?: Iterable<string>): string;
|
|
1
15
|
/**
|
|
2
16
|
* The full security-header set for one response, including the CSP variant.
|
|
3
17
|
* @param {boolean} reactive Whether the route ships the reactive runtime.
|