@zvndev/darkmown 2.6.1-next.12.ge562f5d → 2.7.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.
Files changed (73) hide show
  1. package/AGENTS.md +392 -77
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +316 -49
  4. package/docs/cli.md +51 -17
  5. package/docs/spec-alignment.md +25 -4
  6. package/grammar/wd-directives.gbnf +13 -5
  7. package/package.json +7 -3
  8. package/site/_/base.skin +1 -1
  9. package/site/_/footer.wd +1 -1
  10. package/site/pages/api-errors/index.wd +81 -0
  11. package/site/pages/app.wd +1 -1
  12. package/site/pages/attr-binding/index.wd +84 -0
  13. package/site/pages/blog/darkmown-2-7-0.md +176 -0
  14. package/site/pages/blog/darkmown-2-7-0.skin +127 -0
  15. package/site/pages/blog/no-unsafe-eval.md +66 -0
  16. package/site/pages/blog/no-unsafe-eval.skin +127 -0
  17. package/site/pages/blog/zero-js-by-default.md +1 -1
  18. package/site/pages/branch-state/index.wd +68 -0
  19. package/site/pages/docs/index.wd +320 -19
  20. package/site/pages/index.wd +3 -1
  21. package/site/pages/playground/index.js +63 -56
  22. package/site/pages/pulse/index.wd +1 -1
  23. package/site/pages/settings/index.wd +97 -0
  24. package/site/pages/showcase/index.wd +37 -1
  25. package/site/pages/upload/index.wd +63 -0
  26. package/site/pages/url-state/index.wd +70 -0
  27. package/src/builder.js +222 -34
  28. package/src/catalog.js +217 -21
  29. package/src/cli.js +6 -3
  30. package/src/compiler/actions.js +80 -12
  31. package/src/compiler/body.js +104 -28
  32. package/src/compiler/collections.js +29 -13
  33. package/src/compiler/context.js +9 -2
  34. package/src/compiler/expr-ast.js +14 -0
  35. package/src/compiler/fetch.js +8 -4
  36. package/src/compiler/forms.js +133 -14
  37. package/src/compiler/frontmatter.js +28 -7
  38. package/src/compiler/includes.js +46 -6
  39. package/src/compiler/interpolation.js +56 -1
  40. package/src/compiler/loops.js +489 -35
  41. package/src/compiler/markdown.js +457 -44
  42. package/src/compiler/page.js +8 -3
  43. package/src/compiler/predicates.js +84 -14
  44. package/src/compiler/state.js +167 -18
  45. package/src/compiler/structure.js +159 -29
  46. package/src/errors.js +111 -3
  47. package/src/feeds.js +140 -7
  48. package/src/grammar.js +53 -10
  49. package/src/runtime.js +509 -130
  50. package/src/runtime.min.js +2 -0
  51. package/src/runtime.min.js.map +1 -0
  52. package/src/statics.js +46 -2
  53. package/src/templates/blog/README.md +2 -0
  54. package/src/templates/dashboard/README.md +2 -0
  55. package/src/templates/landing/README.md +2 -0
  56. package/src/templates/starter/README.md +1 -1
  57. package/src/templates/store/README.md +2 -0
  58. package/src/tools/validate.js +14 -4
  59. package/types/builder.d.ts +6 -2
  60. package/types/catalog.d.ts +12 -0
  61. package/types/compiler/actions.d.ts +1 -1
  62. package/types/compiler/body.d.ts +0 -3
  63. package/types/compiler/forms.d.ts +12 -1
  64. package/types/compiler/frontmatter.d.ts +8 -1
  65. package/types/compiler/includes.d.ts +16 -2
  66. package/types/compiler/interpolation.d.ts +23 -0
  67. package/types/compiler/loops.d.ts +47 -0
  68. package/types/compiler/markdown.d.ts +63 -1
  69. package/types/compiler/predicates.d.ts +7 -1
  70. package/types/compiler/state.d.ts +8 -2
  71. package/types/compiler/structure.d.ts +32 -1
  72. package/types/feeds.d.ts +68 -2
  73. package/types/statics.d.ts +25 -0
@@ -59,6 +59,23 @@ export function resolveStateKey(name: string, ctx: Ctx): string | null;
59
59
  * @returns {string}
60
60
  */
61
61
  export function interpolateLeaf(value: unknown, expr: string, ctx: Ctx): string;
62
+ /**
63
+ * Render the INITIAL value of a reactive binding (`data-wd-bind` /
64
+ * `data-wd-each`) as text.
65
+ *
66
+ * Identical to {@link interpolateLeaf} except for arrays: the runtime paints a
67
+ * bind by assigning to `textContent`, which coerces `["a", "b"]` with `String()`
68
+ * to `"a,b"`, and the loop row-template fill already agrees (it escapes through
69
+ * `String()` too). `interpolateLeaf`'s `", "` join is the STATIC contract — the
70
+ * documented `{ meta.tags }` behavior for a value nothing will ever repaint — so
71
+ * the two cannot share one rule. Painting the static form here made the first
72
+ * reactive render silently rewrite `"a, b"` to `"a,b"`.
73
+ * @param {unknown} value
74
+ * @param {string} expr
75
+ * @param {Ctx} ctx
76
+ * @returns {string}
77
+ */
78
+ export function interpolateBound(value: unknown, expr: string, ctx: Ctx): string;
62
79
  /**
63
80
  * JSON-encode a value for an inline `<script>`, escaping `<` to stay HTML-safe.
64
81
  * @param {unknown} value
@@ -96,6 +113,12 @@ export function stripQuotes(value?: string): string;
96
113
  * @returns {string}
97
114
  */
98
115
  export function humanizeName(name: string): string;
116
+ /**
117
+ * True for a value that must never be applied to a bound `href`/`src`.
118
+ * @param {unknown} value
119
+ * @returns {boolean}
120
+ */
121
+ export function unsafeUrlValue(value: unknown): boolean;
99
122
  /**
100
123
  * Escape a value for safe inclusion in HTML text and attribute contexts.
101
124
  * @param {unknown} value
@@ -1,3 +1,27 @@
1
+ /**
2
+ * Serialize an already-validated expression fragment into the compact AST
3
+ * attribute payload the runtime reads, turning a residual parse failure into a
4
+ * CODED compile error with `file:line` instead of the AST layer's raw internal
5
+ * `Error`.
6
+ *
7
+ * `expr-ast.js` re-parses the compiler's own output, so a failure there is not
8
+ * an author mistake and it throws a plain `Error` by design. But the operand
9
+ * folds in `predicates.js` splice BUILD-TIME VALUES into that output, and a
10
+ * value the fold cannot render as a readable literal escapes as an uncoded
11
+ * `expr-ast: …` error with no file, no line, and no suggestion — the one gap in
12
+ * the "every author-facing error carries a code" invariant. This is the seam
13
+ * where that becomes an ordinary Darkmown compile error again.
14
+ *
15
+ * Lives here rather than beside its `structure.js` callers because the error-code
16
+ * registry pins each source file to a subsystem block (`compiler/loops.js` owns
17
+ * WD1xx), and this guard's reserved code is WD190.
18
+ * @param {string} code Validated JS fragment over `I()`/`S()`/`C()`/`A()`.
19
+ * @param {Ctx} ctx
20
+ * @param {number} index 0-based line index of the directive that owns it.
21
+ * @param {string} what Directive label for the message, e.g. `":if"`.
22
+ * @returns {any[]} The parsed AST.
23
+ */
24
+ export function astAt(code: string, ctx: Ctx, index: number, what: string): any[];
1
25
  /**
2
26
  * @param {string} line
3
27
  * @param {string[]} bodyLines
@@ -9,6 +33,26 @@
9
33
  * @returns {string}
10
34
  */
11
35
  export function handleLoop(line: string, bodyLines: string[], emptyLines: string[] | null, ctx: Ctx, index: number, emptyStart?: number): string;
36
+ /**
37
+ * Splice two adjacent tables into one, in the two shapes a `.wd` body produces:
38
+ *
39
+ * 1. Two tables that carry the SAME markup before `<tbody>` — one compiled row
40
+ * body per loop row. Their `<tbody>` contents concatenate. Requiring the
41
+ * prefixes to match is what keeps a header that interpolates the row (a
42
+ * genuinely different table per row) from losing every header but the first.
43
+ * 2. A HEADER-ONLY table (a `<thead>` and no `<tbody>`: what `| Name | N |` +
44
+ * `|---|---|` in the prose above a loop compiles to) followed by a BODY-ONLY
45
+ * table (a `<tbody>` and no `<thead>`: what a loop over bare `|…|` rows now
46
+ * emits). The body moves inside the header's table.
47
+ *
48
+ * Exported because both seams matter: the loop's own row seam (case 1) and the
49
+ * body-level seam between flushed prose and a handler's output (case 2), which
50
+ * `compileBody` owns.
51
+ * @param {string} prev
52
+ * @param {string} next
53
+ * @returns {string | null} The spliced HTML, or null when they do not merge.
54
+ */
55
+ export function spliceTables(prev: string, next: string): string | null;
12
56
  /**
13
57
  * @param {string} str
14
58
  * @param {number} start
@@ -25,12 +69,15 @@ export function matchElement(str: string, start: number, tag: string): number;
25
69
  export function loopKeyOf(item: unknown, counts: Map<string, number>): string;
26
70
  /**
27
71
  * @typedef {import("./context.js").Ctx} Ctx
72
+ * @typedef {import("./context.js").Compilation} Compilation
28
73
  * @typedef {import("./context.js").Predicate} Predicate
29
74
  * @typedef {import("./context.js").NumArg} NumArg
30
75
  * @typedef {import("./context.js").LoopOpts} LoopOpts
31
76
  */
32
77
  export const LOOP_EXAMPLE: "@loop /products.json into p where p.price < 50 sort by p.price asc";
78
+ export function serializeExprAt(code: string, ctx: Ctx, index: number, what: string): string;
33
79
  export type Ctx = import("./context.js").Ctx;
80
+ export type Compilation = import("./context.js").Compilation;
34
81
  export type Predicate = import("./context.js").Predicate;
35
82
  export type NumArg = import("./context.js").NumArg;
36
83
  export type LoopOpts = import("./context.js").LoopOpts;
@@ -5,11 +5,15 @@
5
5
  */
6
6
  export function selectMd(meta?: Meta): MarkdownIt;
7
7
  /**
8
+ * Render one prose chunk.
8
9
  * @param {string} text
9
10
  * @param {Ctx} ctx
11
+ * @param {number} [index] 0-based line index the chunk starts at in the current
12
+ * `compileBody` slice, so a warning about a binding inside it reports the true
13
+ * `file:line`. Defaults to the top of the slice.
10
14
  * @returns {string}
11
15
  */
12
- export function renderProse(text: string, ctx: Ctx): string;
16
+ export function renderProse(text: string, ctx: Ctx, index?: number): string;
13
17
  /**
14
18
  * The inline token an attr block attaches to: the immediately-preceding image
15
19
  * (self-closing) or the open token matching the immediately-preceding close
@@ -29,6 +33,64 @@ export function attrTarget(children: any[], i: number): any;
29
33
  * @returns {string}
30
34
  */
31
35
  export function slugify(text: string): string;
36
+ export { unsafeUrlValue };
37
+ export function encodeDest(value: unknown): string;
38
+ /**
39
+ * A resolved `{ name.path }` binding.
40
+ */
41
+ export type Binding = {
42
+ /**
43
+ * What the inline rule emits: a `data-wd-each` /
44
+ * `data-wd-bind` span for a reactive value, the escaped value for a static one.
45
+ */
46
+ html: string;
47
+ /**
48
+ * The PLAIN resolved text (unescaped, no markup) — a
49
+ * reactive value's initial paint. This is what a link destination, a raw-HTML
50
+ * attribute, and a heading slug need; `html` is unusable in all three.
51
+ */
52
+ text: string;
53
+ /**
54
+ * Where the value came from, and
55
+ * therefore what the non-inline positions may do with it: `static` is a
56
+ * build-time value and always safe to substitute; `state` is `:state`/`:store`
57
+ * and paints its seed then binds; `row` is a reactive `@loop` row, whose ONE
58
+ * template serves every row, so it paints nothing and binds per row.
59
+ */
60
+ kind: "static" | "state" | "row";
61
+ /**
62
+ * For `state`: the fully-qualified state key.
63
+ */
64
+ key?: string | undefined;
65
+ /**
66
+ * For `state`/`row`: the dotted sub-path under it ("" for the whole value).
67
+ */
68
+ path?: string | undefined;
69
+ /**
70
+ * For `row`: the per-row meta variable (`index`, `number`, …).
71
+ */
72
+ meta?: string | undefined;
73
+ };
32
74
  export type Meta = import("./context.js").Meta;
33
75
  export type Ctx = import("./context.js").Ctx;
76
+ /**
77
+ * One reactive binding found in a destination: the serialized template part the
78
+ * runtime evaluates, plus the build-time text to paint for it.
79
+ */
80
+ export type PendingAttr = {
81
+ /**
82
+ * `s` = state key + sub-path, `i` = loop-row sub-path, `m` = row meta variable.
83
+ */
84
+ part: ["s", string, string] | ["i", string] | ["m", string];
85
+ /**
86
+ * Build-time paint ("" for anything per-row).
87
+ */
88
+ text: string;
89
+ /**
90
+ * The author's own `{ … }` source, restored when the
91
+ * `](…)` turned out not to be a link after all.
92
+ */
93
+ raw: string;
94
+ };
34
95
  import MarkdownIt from "markdown-it";
96
+ import { unsafeUrlValue } from "./interpolation.js";
@@ -8,12 +8,18 @@ export function compilePredicate(raw: string, itemName: string, ctx: Ctx): Predi
8
8
  /**
9
9
  * Evaluate a compiled predicate against a row at build time by walking its AST —
10
10
  * the same closed evaluator the runtime uses, so the fold matches. No eval.
11
+ *
12
+ * `what` names the CONSTRUCT the body came from. Three directives share this
13
+ * evaluator (`@loop … where`, `::: … .class when`, `:if`), and the warning used
14
+ * to say "@loop where predicate … treating the row as excluded" for all of
15
+ * them — pointing an author at a loop they never wrote when a `:if` folded.
11
16
  * @param {string} body
12
17
  * @param {unknown} item
13
18
  * @param {Ctx} ctx
19
+ * @param {string} [what] Directive label, e.g. `":if"`. Defaults to `@loop where`.
14
20
  * @returns {boolean}
15
21
  */
16
- export function evalPredicate(body: string, item: unknown, ctx: Ctx): boolean;
22
+ export function evalPredicate(body: string, item: unknown, ctx: Ctx, what?: string): boolean;
17
23
  /**
18
24
  * Compile a `::: … .class when <predicate>`. Allows `:if`-style bare truthy
19
25
  * paths and `where`-style `left <op> right` conditions joined by `and`/`or`.
@@ -31,8 +31,13 @@ export function handleStore(line: string, ctx: Ctx, index: number): string;
31
31
  */
32
32
  export function declareStore(name: string, value: unknown, ctx: Ctx): string;
33
33
  /**
34
- * Seed a `<name>_error` state key (null) if absent. Shared by :fetch and the
35
- * round-trip :form so error fallbacks have a key to bind.
34
+ * Seed the `<name>_error` / `<name>_error_body` pair (null) if absent. Shared by
35
+ * `:fetch` and the round-trip `:form` so error fallbacks have a key to bind.
36
+ *
37
+ * `_error` is what the server SAID — its JSON `error`/`message` field when the
38
+ * body has one, else `HTTP <status>`. `_error_body` is the whole parsed body,
39
+ * so a page can render field-level errors (`{ signup_error_body.fields.email }`)
40
+ * without a colocated script.
36
41
  * @param {string} key
37
42
  * @param {Ctx} ctx
38
43
  * @returns {void}
@@ -65,4 +70,5 @@ export const STATE_EXAMPLE: ":state count = 0";
65
70
  export const STORE_EXAMPLE: ":store cart = []";
66
71
  export const COMPUTED_EXAMPLE: ":computed total = items.length * 4";
67
72
  export const THEME_EXAMPLE: ":theme";
73
+ export const URL_STATE_EXAMPLE: ":state q = \"\" from-url";
68
74
  export type Ctx = import("./context.js").Ctx;
@@ -1,3 +1,28 @@
1
+ /**
2
+ * Peel the leading run of whitelisted accessibility attributes off `rest`.
3
+ *
4
+ * Stops at the first token that does not open like an attribute (a `.class`, a
5
+ * `#id`, a `->`, or the end of the line) and hands that remainder back. A token
6
+ * that DOES open like an attribute but is misspelt, unquoted, or outside the
7
+ * whitelist is a compile error — never silently skipped.
8
+ * @param {string} rest Everything from the cursor to the end of the line.
9
+ * @param {Ctx} ctx
10
+ * @param {number} index 0-based line index for `file:line` errors.
11
+ * @param {string} what Directive label for the message, e.g. `:button`.
12
+ * @returns {{ attrs: [string, string][], rest: string }}
13
+ */
14
+ export function takeA11yAttrs(rest: string, ctx: Ctx, index: number, what: string): {
15
+ attrs: [string, string][];
16
+ rest: string;
17
+ };
18
+ /**
19
+ * Serialize validated attribute pairs into an HTML attribute string. The NAME
20
+ * came through {@link A11Y_ATTR_NAME} so it is inert; the VALUE is author text
21
+ * and is escaped.
22
+ * @param {[string, string][]} attrs
23
+ * @returns {string}
24
+ */
25
+ export function a11yAttrHtml(attrs: [string, string][]): string;
1
26
  /**
2
27
  * @param {string} line
3
28
  * @param {Ctx} ctx
@@ -40,7 +65,10 @@ export function handleCarousel(line: string, bodyLines: string[], ctx: Ctx, inde
40
65
  */
41
66
  export function handleIf(line: string, truthyLines: string[], falsyLines: string[], ctx: Ctx, index: number, falsyStart?: number): string;
42
67
  /**
43
- * Render the documentation-demo directives (`:try`, `:note`, `:sprint`).
68
+ * Render the documentation-demo directive `:try` (the "Try it" card the homepage
69
+ * uses). `:note` and `:sprint` lived here too and were deleted: nothing in the
70
+ * site, the docs, or the public directive set used them, and a directive the
71
+ * catalog does not list is a trap for an AI author that discovers it in source.
44
72
  * @param {string} line
45
73
  * @param {Ctx} ctx
46
74
  * @returns {string}
@@ -51,6 +79,9 @@ export function renderDemoDirective(line: string, ctx: Ctx): string;
51
79
  */
52
80
  export const INCLUDE_EXAMPLE: "@include /header.wd";
53
81
  export const CONTAINER_EXAMPLE: "::: card .featured";
82
+ export const CONTAINER_A11Y_EXAMPLE: "::: card .note role=\"region\" aria-label=\"Notes\"";
54
83
  export const IF_EXAMPLE: ":if count > 0";
55
84
  export const CAROUSEL_EXAMPLE: ":carousel autoplay=3000";
85
+ /** The corrective `Use:` tail every attribute error carries. */
86
+ export const A11Y_ATTR_USE: "role=\"\u2026\", aria-\u2026=\"\u2026\", or title=\"\u2026\" (quoted static text)";
56
87
  export type Ctx = import("./context.js").Ctx;
package/types/feeds.d.ts CHANGED
@@ -75,16 +75,58 @@ export function firstParagraph(body: string): string;
75
75
  * @returns {string}
76
76
  */
77
77
  export function rssDescription(meta: import("./compiler.js").Meta, file: string, body: string): string;
78
+ /**
79
+ * Validate a frontmatter `date:` into a sitemap `<lastmod>` value, or "" when it
80
+ * is not one.
81
+ *
82
+ * This is the only feed value that used to be neither validated nor escaped, and
83
+ * both halves mattered: `date: 2026<bad&"q"` made the whole document
84
+ * unparseable, so NOTHING got indexed, and the far likelier `date: Jan 5, 2026`
85
+ * was silently truncated to `Jan 5, 202` by a blind 10-character slice. Returning
86
+ * "" (rather than guessing) lets the caller omit the element and say so — a
87
+ * sitemap entry with no `<lastmod>` is valid; one with a bogus date is not.
88
+ * @param {string} raw Trimmed frontmatter `date:` value.
89
+ * @returns {string} `yyyy-mm-dd`, or "" when `raw` is not a real calendar date.
90
+ */
91
+ export function sitemapDate(raw: string): string;
78
92
  /**
79
93
  * Build `sitemap.xml` from the emitted (post-draft-filter) routes. One `<url>`
80
94
  * per route — reactive pages included (they're indexable HTML) — with `<loc>` =
81
- * `site_url` + path and `<lastmod>` from the supplied date. The `/404/` route is
95
+ * `site_url` + path and `<lastmod>` from the supplied date. An empty `lastmod`
96
+ * omits the element rather than emitting a blank one. The `/404/` route is
82
97
  * excluded by the caller before this point. No `<priority>`/`<changefreq>` —
83
98
  * Google ignores them and they invite bit-rot.
84
99
  * @param {DatedRoute[]} entries Absolute loc + ISO lastmod, in route order.
85
100
  * @returns {string} The full XML document (trailing newline).
86
101
  */
87
102
  export function buildSitemap(entries: DatedRoute[]): string;
103
+ /**
104
+ * Build a `<sitemapindex>` — the document that points at the numbered sitemap
105
+ * shards. No `<lastmod>` per shard: it would have to be the max over the shard's
106
+ * entries, which is a number nobody consumes and one more thing to keep true.
107
+ * @param {string[]} locs Absolute URLs of the shard files, in order.
108
+ * @returns {string} The full XML document (trailing newline).
109
+ */
110
+ export function buildSitemapIndex(locs: string[]): string;
111
+ /**
112
+ * Plan the sitemap file(s) a set of entries needs.
113
+ *
114
+ * At or under {@link SITEMAP_URL_LIMIT} that is one `sitemap.xml`, byte-identical
115
+ * to what every site got before. Over it, the entries shard into
116
+ * `sitemap-1.xml`, `sitemap-2.xml`, … and `sitemap.xml` becomes the
117
+ * `<sitemapindex>` pointing at them — so `robots.txt` keeps naming
118
+ * `/sitemap.xml` and every existing submission stays valid.
119
+ *
120
+ * Returned rather than written so the split is a pure function the tests can
121
+ * drive with 50,001 synthetic entries without building 50,001 pages.
122
+ * @param {DatedRoute[]} entries
123
+ * @param {string} siteUrl Absolute origin, for the index's shard URLs.
124
+ * @returns {{ file: string, xml: string }[]} Shards first, `sitemap.xml` last.
125
+ */
126
+ export function sitemapDocuments(entries: DatedRoute[], siteUrl: string): {
127
+ file: string;
128
+ xml: string;
129
+ }[];
88
130
  /**
89
131
  * An RSS item — a dated post in feed order.
90
132
  * @typedef {object} RssItem
@@ -131,6 +173,26 @@ export function buildRobots(siteUrl?: string, policy?: string): string;
131
173
  * @returns {string} One of {@link AI_CRAWLER_POLICIES}.
132
174
  */
133
175
  export function aiCrawlerPolicy(meta: import("./compiler.js").Meta, file?: string): string;
176
+ /**
177
+ * Resolve the site's RSS item cap from the HOME page's `rss_limit:` frontmatter.
178
+ *
179
+ * Darkmown has no config file by design: site-wide settings live in the home
180
+ * page's frontmatter next to `site_url` / `ai_crawlers`, and this is one of them.
181
+ * Absent means {@link RSS_ITEM_LIMIT}. A value that is not a positive integer
182
+ * THROWS rather than silently falling back, for the same reason `ai_crawlers`
183
+ * does: `rss_limit: 0` and `rss_limit: all` are both attempts to state a policy,
184
+ * and quietly publishing 20 items instead is the wrong failure direction.
185
+ * @param {import("./compiler.js").Meta} meta Home page frontmatter.
186
+ * @param {string} [file] Home page source path, for the error message.
187
+ * @returns {number} A positive integer.
188
+ */
189
+ export function rssItemLimit(meta: import("./compiler.js").Meta, file?: string): number;
190
+ /**
191
+ * The sitemap protocol's hard cap: 50,000 `<url>` entries per sitemap file. Over
192
+ * it, Google rejects the document outright — so a large site must split into
193
+ * numbered sitemaps behind a `<sitemapindex>`.
194
+ */
195
+ export const SITEMAP_URL_LIMIT: 50000;
134
196
  /**
135
197
  * The AI crawler and answer-engine robots.txt tokens Darkmown names explicitly.
136
198
  *
@@ -159,7 +221,11 @@ export const AI_CRAWLERS: {
159
221
  }[];
160
222
  /** The accepted `ai_crawlers:` frontmatter values. */
161
223
  export const AI_CRAWLER_POLICIES: string[];
162
- /** Most-recent-first RSS post cap — a feed is a recent window, not an archive. */
224
+ /**
225
+ * The DEFAULT most-recent-first RSS post cap — a feed is a recent window, not an
226
+ * archive. Overridable per site with `rss_limit:` on the home page
227
+ * ({@link rssItemLimit}).
228
+ */
163
229
  export const RSS_ITEM_LIMIT: 20;
164
230
  export type Route = import("./router.js").Route;
165
231
  /**
@@ -6,6 +6,31 @@
6
6
  * @returns {void}
7
7
  */
8
8
  export function serve(distRoot: string, url: string, res: http.ServerResponse): void;
9
+ /**
10
+ * Whether a resolved path is something we can actually stream back: a regular
11
+ * file. A DIRECTORY is the case that matters — `fs.existsSync` says yes to one,
12
+ * and handing it to a read stream is an `EISDIR` on an unhandled `error` event,
13
+ * which takes the whole server process down. Treating it as a miss answers 404
14
+ * instead. Anything unstattable (absent, or a parent the process cannot read)
15
+ * is a miss too.
16
+ * @param {string} file Absolute path inside the build output.
17
+ * @returns {boolean}
18
+ */
19
+ export function isServableFile(file: string): boolean;
20
+ /**
21
+ * Stream a built file to the response with an `error` handler attached.
22
+ *
23
+ * `fs.createReadStream(file).pipe(res)` has none, so ANY read failure — a file
24
+ * deleted by a rebuild between the stat and the open, a permission error, a
25
+ * path that turned out not to be a regular file — is an unhandled `error` event
26
+ * and an immediate process exit. Here it answers instead: a 500 when the
27
+ * response has not started, and a clean end when it has (the status is already
28
+ * on the wire by then, so the truncated body is all we can signal with).
29
+ * @param {string} file Absolute path to the file to stream.
30
+ * @param {http.ServerResponse} res Response to write to.
31
+ * @returns {void}
32
+ */
33
+ export function pipeFile(file: string, res: http.ServerResponse): void;
9
34
  /**
10
35
  * Resolve a request URL to an absolute file inside `distRoot`, guarding against
11
36
  * path traversal. Returns null when the resolved path escapes the root.