@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.
- package/AGENTS.md +392 -77
- package/CHANGELOG.md +47 -0
- package/README.md +316 -49
- package/docs/cli.md +51 -17
- package/docs/spec-alignment.md +25 -4
- package/grammar/wd-directives.gbnf +13 -5
- package/package.json +7 -3
- package/site/_/base.skin +1 -1
- package/site/_/footer.wd +1 -1
- package/site/pages/api-errors/index.wd +81 -0
- package/site/pages/app.wd +1 -1
- package/site/pages/attr-binding/index.wd +84 -0
- package/site/pages/blog/darkmown-2-7-0.md +176 -0
- package/site/pages/blog/darkmown-2-7-0.skin +127 -0
- package/site/pages/blog/no-unsafe-eval.md +66 -0
- package/site/pages/blog/no-unsafe-eval.skin +127 -0
- package/site/pages/blog/zero-js-by-default.md +1 -1
- package/site/pages/branch-state/index.wd +68 -0
- package/site/pages/docs/index.wd +320 -19
- package/site/pages/index.wd +3 -1
- package/site/pages/playground/index.js +63 -56
- package/site/pages/pulse/index.wd +1 -1
- package/site/pages/settings/index.wd +97 -0
- package/site/pages/showcase/index.wd +37 -1
- package/site/pages/upload/index.wd +63 -0
- package/site/pages/url-state/index.wd +70 -0
- package/src/builder.js +222 -34
- package/src/catalog.js +217 -21
- package/src/cli.js +6 -3
- package/src/compiler/actions.js +80 -12
- package/src/compiler/body.js +104 -28
- package/src/compiler/collections.js +29 -13
- package/src/compiler/context.js +9 -2
- package/src/compiler/expr-ast.js +14 -0
- package/src/compiler/fetch.js +8 -4
- package/src/compiler/forms.js +133 -14
- package/src/compiler/frontmatter.js +28 -7
- package/src/compiler/includes.js +46 -6
- package/src/compiler/interpolation.js +56 -1
- package/src/compiler/loops.js +489 -35
- package/src/compiler/markdown.js +457 -44
- package/src/compiler/page.js +8 -3
- package/src/compiler/predicates.js +84 -14
- package/src/compiler/state.js +167 -18
- package/src/compiler/structure.js +159 -29
- package/src/errors.js +111 -3
- package/src/feeds.js +140 -7
- package/src/grammar.js +53 -10
- package/src/runtime.js +509 -130
- package/src/runtime.min.js +2 -0
- package/src/runtime.min.js.map +1 -0
- package/src/statics.js +46 -2
- package/src/templates/blog/README.md +2 -0
- package/src/templates/dashboard/README.md +2 -0
- package/src/templates/landing/README.md +2 -0
- package/src/templates/starter/README.md +1 -1
- package/src/templates/store/README.md +2 -0
- package/src/tools/validate.js +14 -4
- package/types/builder.d.ts +6 -2
- package/types/catalog.d.ts +12 -0
- package/types/compiler/actions.d.ts +1 -1
- package/types/compiler/body.d.ts +0 -3
- package/types/compiler/forms.d.ts +12 -1
- package/types/compiler/frontmatter.d.ts +8 -1
- package/types/compiler/includes.d.ts +16 -2
- package/types/compiler/interpolation.d.ts +23 -0
- package/types/compiler/loops.d.ts +47 -0
- package/types/compiler/markdown.d.ts +63 -1
- package/types/compiler/predicates.d.ts +7 -1
- package/types/compiler/state.d.ts +8 -2
- package/types/compiler/structure.d.ts +32 -1
- package/types/feeds.d.ts +68 -2
- 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
|
|
35
|
-
* round-trip
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
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
|
/**
|
package/types/statics.d.ts
CHANGED
|
@@ -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.
|