@zvndev/darkmown 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +34 -2
  3. package/docs/spec-alignment.md +10 -0
  4. package/package.json +16 -11
  5. package/site/pages/docs/index.wd +14 -0
  6. package/site/pages/playground/index.js +47 -46
  7. package/src/catalog.js +16 -1
  8. package/src/compiler/actions.js +42 -22
  9. package/src/compiler/body.js +36 -14
  10. package/src/compiler/collections.js +16 -10
  11. package/src/compiler/context.js +39 -10
  12. package/src/compiler/fetch.js +39 -15
  13. package/src/compiler/format.js +5 -2
  14. package/src/compiler/forms.js +91 -26
  15. package/src/compiler/frontmatter.js +11 -2
  16. package/src/compiler/fs-reader.js +2 -1
  17. package/src/compiler/image-size.js +340 -0
  18. package/src/compiler/includes.js +8 -4
  19. package/src/compiler/interpolation.js +15 -7
  20. package/src/compiler/loops.js +63 -35
  21. package/src/compiler/markdown.js +4 -3
  22. package/src/compiler/media.js +17 -3
  23. package/src/compiler/page.js +8 -6
  24. package/src/compiler/predicates.js +49 -24
  25. package/src/compiler/reader.js +12 -2
  26. package/src/compiler/state.js +16 -2
  27. package/src/compiler/structure.js +24 -14
  28. package/src/deploy.js +8 -4
  29. package/src/errors.js +1156 -0
  30. package/src/router.js +7 -4
  31. package/src/scaffold.js +4 -2
  32. package/src/skin.js +17 -6
  33. package/types/api-runner.d.ts +94 -0
  34. package/types/builder.d.ts +141 -10
  35. package/types/catalog.d.ts +97 -0
  36. package/types/compiler/actions.d.ts +41 -0
  37. package/types/compiler/body.d.ts +11 -0
  38. package/types/compiler/collections.d.ts +76 -0
  39. package/types/compiler/context.d.ts +661 -0
  40. package/types/compiler/directives.d.ts +6 -0
  41. package/types/compiler/expr-ast.d.ts +20 -0
  42. package/types/compiler/fetch.d.ts +29 -0
  43. package/types/compiler/format.d.ts +49 -0
  44. package/types/compiler/forms.d.ts +94 -0
  45. package/types/compiler/frontmatter.d.ts +33 -0
  46. package/types/compiler/fs-reader.d.ts +7 -0
  47. package/types/compiler/image-size.d.ts +14 -0
  48. package/types/compiler/includes.d.ts +100 -0
  49. package/types/compiler/index.d.ts +5 -0
  50. package/types/compiler/interpolation.d.ts +115 -0
  51. package/types/compiler/loops.d.ts +36 -0
  52. package/types/compiler/markdown.d.ts +34 -0
  53. package/types/compiler/media.d.ts +32 -0
  54. package/types/compiler/page.d.ts +109 -0
  55. package/types/compiler/predicates.d.ts +50 -0
  56. package/types/compiler/reader.d.ts +56 -0
  57. package/types/compiler/state.d.ts +68 -0
  58. package/types/compiler/structure.d.ts +56 -0
  59. package/types/compiler.d.ts +35 -223
  60. package/types/dev.d.ts +47 -2
  61. package/types/errors.d.ts +100 -0
  62. package/types/feeds.d.ts +150 -0
  63. package/types/headers.d.ts +44 -0
  64. package/types/highlight.d.ts +24 -0
  65. package/types/router.d.ts +42 -3
  66. package/types/scaffold.d.ts +15 -3
  67. package/types/skin.d.ts +28 -1
  68. package/types/statics.d.ts +2 -1
package/src/router.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { wdError } from "./compiler/context.js";
3
4
  import { parseFrontmatter } from "./compiler.js";
4
5
  import { isHiddenName, pageExtensions } from "./config.js";
5
6
 
@@ -64,8 +65,9 @@ export function discoverRoutes(routesRoot, options = {}) {
64
65
  const seen = new Map();
65
66
  for (const route of routes) {
66
67
  if (seen.has(route.route)) {
67
- throw new Error(
68
- `Duplicate route "${route.route}" from ${seen.get(route.route)} and ${route.file}`
68
+ throw wdError(
69
+ `Duplicate route "${route.route}" from ${seen.get(route.route)} and ${route.file}`,
70
+ { code: "WD901", file: route.file }
69
71
  );
70
72
  }
71
73
  seen.set(route.route, route.file);
@@ -114,8 +116,9 @@ export function outputPathForRoute(distRoot, route) {
114
116
  const clean = route === "/" ? "" : route.replace(/^\/|\/$/g, "");
115
117
  const out = path.join(distRoot, clean, "index.html");
116
118
  if (!path.resolve(out).startsWith(path.resolve(distRoot) + path.sep)) {
117
- throw new Error(
118
- `Route "${route}" resolves outside the build output directory ${distRoot} — routes must not contain traversal segments`
119
+ throw wdError(
120
+ `Route "${route}" resolves outside the build output directory ${distRoot} — routes must not contain traversal segments`,
121
+ { code: "WD902" }
119
122
  );
120
123
  }
121
124
  return out;
package/src/scaffold.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { wdError } from "./compiler/context.js";
4
5
 
5
6
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
6
7
  const templatesDir = path.join(moduleDir, "templates");
@@ -36,9 +37,10 @@ export function initProject(root, options = {}) {
36
37
  const templateDir = path.join(templatesDir, template);
37
38
  if (!fs.existsSync(templateDir) || !fs.statSync(templateDir).isDirectory()) {
38
39
  const list = availableTemplates().join(", ");
39
- throw new Error(
40
+ throw wdError(
40
41
  `Unknown template "${template}". Available templates: ${list || "(none)"}. ` +
41
- "Use: darkmown init [dir] --template <name>"
42
+ "Use: darkmown init [dir] --template <name>",
43
+ { code: "WD903", hint: "darkmown init [dir] --template <name>" }
42
44
  );
43
45
  }
44
46
 
package/src/skin.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { wdError } from "./compiler/context.js";
2
3
 
3
4
  const aliases = new Map([
4
5
  ["radius", "border-radius"],
@@ -96,7 +97,7 @@ export function compileSkin(source, opts = {}) {
96
97
  }
97
98
  for (let i = start; i < lines.length; i++) {
98
99
  if (lines[i].text === "scoped" && lines[i].indent === 0) {
99
- throw new Error('"scoped" must be the first line of a .skin file');
100
+ throw wdError('"scoped" must be the first line of a .skin file', { code: "WD801" });
100
101
  }
101
102
  }
102
103
 
@@ -184,8 +185,9 @@ export function compileSkin(source, opts = {}) {
184
185
  // place a filtered bare `*`/`page` reset would otherwise leak through to), so
185
186
  // it errors with the same hint instead of emitting a never-matching rule.
186
187
  if (scope && current === ":root") {
187
- throw new Error(
188
- `page-level declaration "${text}" is not allowed in a scoped .skin — page-level styles belong in a global skin, not a scoped one`
188
+ throw wdError(
189
+ `page-level declaration "${text}" is not allowed in a scoped .skin — page-level styles belong in a global skin, not a scoped one`,
190
+ { code: "WD802" }
189
191
  );
190
192
  }
191
193
  const media = stack.find((frame) => frame.media)?.media;
@@ -235,8 +237,9 @@ function guardPageLevel(text, parent) {
235
237
  for (const part of text.split(",")) {
236
238
  const clean = part.trim();
237
239
  if (PAGE_LEVEL_SELECTORS.has(clean)) {
238
- throw new Error(
239
- `page-level selector "${clean}" is not allowed in a scoped .skin — page-level styles belong in a global skin, not a scoped one`
240
+ throw wdError(
241
+ `page-level selector "${clean}" is not allowed in a scoped .skin — page-level styles belong in a global skin, not a scoped one`,
242
+ { code: "WD803" }
240
243
  );
241
244
  }
242
245
  }
@@ -308,7 +311,15 @@ function subjectBase(base) {
308
311
  */
309
312
  function normalizeAttr(bracketed) {
310
313
  const m = bracketed.match(/^\[\s*([^\]=\s]+)\s*=\s*"?([^\]"]*)"?\s*\]$/);
311
- return m ? `[${m[1]}="${m[2]}"]` : null;
314
+ if (!m) return null;
315
+ // The value is quoted on the way out, but the NAME lands in the selector
316
+ // verbatim — so it must be validated, not just captured. Without this an
317
+ // input like `tokens [{=v]` emitted `:root[{="v"]`, whose stray brace
318
+ // unbalances the stylesheet and makes a CSS parser swallow every rule that
319
+ // follows it. Anything that is not a plain CSS identifier falls through to
320
+ // the caller's `:root` fallback. (Found by tests/fuzz-skin.test.js.)
321
+ if (!/^[A-Za-z_-][\w-]*$/.test(m[1])) return null;
322
+ return `[${m[1]}="${m[2]}"]`;
312
323
  }
313
324
 
314
325
  /**
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The local-dev serverless API runner. Gives `darkmown dev` the SAME `/api/*`
3
+ * behavior a project gets on Vercel / Cloudflare in production, so a `:fetch
4
+ * /api/x` or a form POST works identically before you ever deploy.
5
+ *
6
+ * The convention is deliberately plain: a backend endpoint is a plain-JS file
7
+ * under the project's top-level `api/` directory that default-exports a
8
+ * Web-standard handler `(request: Request, context) => Response | Promise<Response>`.
9
+ * That is the exact signature Vercel Edge, Cloudflare Pages, Netlify Edge and
10
+ * Deno already run — Darkmown owns no server, it just emulates the host locally.
11
+ *
12
+ * File → route, rooted at the `/api` URL prefix (matching Vercel's `api/`):
13
+ * api/echo.js → /api/echo
14
+ * api/users/index.js → /api/users
15
+ * api/users/list.js → /api/users/list
16
+ * api/users/[id].js → /api/users/:id (context.params.id)
17
+ *
18
+ * Nothing here mutates module state: routes are discovered per call so a newly
19
+ * added function is picked up without a restart, and each handler is imported
20
+ * with an mtime cache-buster so edits hot-reload.
21
+ */
22
+ /**
23
+ * A discovered API route.
24
+ * @typedef {object} ApiRoute
25
+ * @property {string} file Absolute path to the handler module.
26
+ * @property {string[]} segments Route segments after `/api`; a dynamic segment
27
+ * is stored as `[name]` (e.g. `["users", "[id]"]`).
28
+ */
29
+ /**
30
+ * Walk `apiDir` and return every `.js`/`.mjs` handler as an {@link ApiRoute}.
31
+ * Files/dirs whose name starts with `.`, `_`, or `-` are treated as private
32
+ * (helpers, fixtures) and never routed, matching the router's hidden-path rule.
33
+ * Returns `[]` when `apiDir` does not exist.
34
+ * @param {string} apiDir Absolute path to the project's `api/` directory.
35
+ * @returns {ApiRoute[]}
36
+ */
37
+ export function discoverApiRoutes(apiDir: string): ApiRoute[];
38
+ /**
39
+ * Match a request pathname against discovered routes. The pathname is expected
40
+ * to start with `/api`; anything else returns `null` (caller serves static).
41
+ * @param {ApiRoute[]} routes
42
+ * @param {string} pathname Request pathname (no query string).
43
+ * @returns {{ file: string, params: Record<string, string> } | null}
44
+ */
45
+ export function matchApiRoute(routes: ApiRoute[], pathname: string): {
46
+ file: string;
47
+ params: Record<string, string>;
48
+ } | null;
49
+ /**
50
+ * Build a Web-standard `Request` from a Node `IncomingMessage`. The body (for
51
+ * methods that carry one) is buffered fully — dev endpoints are small and this
52
+ * avoids the `duplex: "half"` streaming dance.
53
+ * @param {import("node:http").IncomingMessage} req
54
+ * @returns {Promise<Request>}
55
+ */
56
+ export function nodeRequestToWeb(req: import("node:http").IncomingMessage): Promise<Request>;
57
+ /**
58
+ * Write a Web-standard `Response` back onto a Node `ServerResponse`.
59
+ * @param {Response} response
60
+ * @param {import("node:http").ServerResponse} res
61
+ * @returns {Promise<void>}
62
+ */
63
+ export function writeWebResponse(response: Response, res: import("node:http").ServerResponse): Promise<void>;
64
+ /**
65
+ * Handle a request as an API call if its pathname matches a function under
66
+ * `apiDir`. Returns `true` when it owned the request (a route matched, whether
67
+ * the handler succeeded or threw), `false` when nothing matched so the caller
68
+ * should fall through to static serving.
69
+ *
70
+ * @param {object} io
71
+ * @param {string} io.apiDir Absolute path to the project's `api/` directory.
72
+ * @param {import("node:http").IncomingMessage} io.req
73
+ * @param {import("node:http").ServerResponse} io.res
74
+ * @returns {Promise<boolean>}
75
+ */
76
+ export function handleApiRequest({ apiDir, req, res }: {
77
+ apiDir: string;
78
+ req: import("node:http").IncomingMessage;
79
+ res: import("node:http").ServerResponse;
80
+ }): Promise<boolean>;
81
+ /**
82
+ * A discovered API route.
83
+ */
84
+ export type ApiRoute = {
85
+ /**
86
+ * Absolute path to the handler module.
87
+ */
88
+ file: string;
89
+ /**
90
+ * Route segments after `/api`; a dynamic segment
91
+ * is stored as `[name]` (e.g. `["users", "[id]"]`).
92
+ */
93
+ segments: string[];
94
+ };
@@ -1,24 +1,127 @@
1
1
  /**
2
2
  * Compile the whole site under `cwd` into `dist`, returning the route manifest.
3
+ *
4
+ * The build is always 100% static, CDN-cacheable HTML. Backend `api/*.js`
5
+ * functions are NOT compiled — they are the host's concern. `target` only
6
+ * decides the small amount of host glue Darkmown emits alongside the static
7
+ * output: the default (`vercel`/anything) leaves `api/` for the platform to pick
8
+ * up natively; `cloudflare` emits a `dist/_worker.js/` module that routes
9
+ * `/api/*` to those same functions and serves everything else as a static asset.
10
+ *
3
11
  * @param {string} [cwd] Project working directory (defaults to `process.cwd()`).
4
- * @returns {{ routes: RouteManifestEntry[], distRoot: string }}
12
+ * @param {{ target?: string, includeDrafts?: boolean, depMap?: boolean, changed?: string[], quietFeedHints?: boolean }} [options]
13
+ * `target: "cloudflare"` emits the Pages worker. `includeDrafts: true` keeps
14
+ * `draft: true` pages in the build + feeds (dev / `build --drafts`); default
15
+ * excludes them at route discovery so feeds never see a draft. `depMap: true`
16
+ * (dev builds) writes the per-route dependency map to `dist/.wd-dev-deps.json`.
17
+ * `changed` (project-relative paths, dev only; implies `depMap`) attempts an
18
+ * incremental rebuild of just the affected routes, falling back to a full
19
+ * rebuild on ANY uncertainty — a stale page is worse than a slow rebuild.
20
+ * @returns {BuildResult}
5
21
  */
6
- export function buildSite(cwd?: string): {
7
- routes: RouteManifestEntry[];
8
- distRoot: string;
9
- };
22
+ export function buildSite(cwd?: string, options?: {
23
+ target?: string;
24
+ includeDrafts?: boolean;
25
+ depMap?: boolean;
26
+ changed?: string[];
27
+ quietFeedHints?: boolean;
28
+ }): BuildResult;
10
29
  /**
11
- * Strip JSDoc `/** ... *\/` blocks from the runtime before it ships.
12
- * The source keeps full type annotations (for `checkJs` + `.d.ts`), but the
13
- * browser never needs to download them — this keeps the shipped runtime lean
14
- * and the gzip budget honest (measured against what users actually receive).
15
- * Only `/** ... *\/` blocks are removed; code and string literals are untouched.
30
+ * Strip developer comments from the runtime before it ships. The source keeps
31
+ * full JSDoc type annotations (for `checkJs` + `.d.ts`) and explanatory comments
32
+ * so it stays readable; the browser downloads neither. This keeps the shipped
33
+ * runtime lean and the gzip budget honest (measured against what users receive),
34
+ * WITHOUT minifying — identifiers, structure, and whitespace are all preserved.
35
+ *
36
+ * Two passes, each conservative: `/** ... *\/` JSDoc blocks, then whole-line
37
+ * `//` comments (a line that is nothing but a comment). Trailing `//` after code
38
+ * and `//` inside strings/URLs are deliberately left untouched, so no string
39
+ * literal is ever harmed.
16
40
  * @param {string} source
17
41
  * @returns {string}
18
42
  */
19
43
  export function stripRuntimeComments(source: string): string;
44
+ /**
45
+ * Render the Cloudflare Pages `_headers` file from the route manifest.
46
+ *
47
+ * Cloudflare applies every matching block in order; when blocks set the same
48
+ * header, the later block wins. So we emit the catch-all `/*` first (baseline
49
+ * security headers + the reactive CSP, which since 2.1 is eval-free and satisfies
50
+ * every page), then one block per static route that re-states the same eval-free
51
+ * CSP as defense-in-depth. Reactive routes need no override — the catch-all
52
+ * already carries the eval-free CSP.
53
+ *
54
+ * Clean URLs mean a route like `/docs/` is served at both `/docs` and `/docs/`,
55
+ * so each static route emits a path glob (`/docs`, `/docs/*`) that covers both.
56
+ * @param {RouteManifestEntry[]} manifest
57
+ * @returns {string}
58
+ */
59
+ export function renderCloudflareHeaders(manifest: RouteManifestEntry[]): string;
60
+ /**
61
+ * @typedef {import("./config.js").Paths} Paths
62
+ * @typedef {import("./compiler.js").Assets} Assets
63
+ * @typedef {import("./compiler.js").Meta} Meta
64
+ * @typedef {import("./router.js").Route} Route
65
+ */
66
+ /**
67
+ * A built route entry written to `dist/routes.json`.
68
+ * @typedef {object} RouteManifestEntry
69
+ * @property {string} route Public route path.
70
+ * @property {string} file Source file path, relative to cwd, POSIX-separated.
71
+ * @property {{ skins: string[], scripts: string[], runtime: boolean }} assets
72
+ */
73
+ /**
74
+ * One source route's record in the dev dependency map (`dist/.wd-dev-deps.json`),
75
+ * written by dev builds so a later `--changed` build can rebuild only the routes
76
+ * whose dependency graph contains the changed file.
77
+ * @typedef {object} DepMapEntry
78
+ * @property {string} file Source file, project-relative, POSIX-separated.
79
+ * @property {string[]} deps Every source file this route's compile read (the page
80
+ * itself, includes, loop JSON data files, colocated `.skin`/`.js`) —
81
+ * project-relative, POSIX-separated, sorted.
82
+ * @property {string[]} collections Collection names the route loops, so a change
83
+ * to any entry of the collection rebuilds this route.
84
+ * @property {RouteManifestEntry[]} pages The manifest entries this route emitted
85
+ * (one, or one per pagination page), so `routes.json`/`_headers`/the sitemap
86
+ * can be reassembled globally without recompiling untouched routes.
87
+ */
88
+ /** The dev dependency map's filename under `dist/`. */
89
+ export const DEP_MAP_FILE: ".wd-dev-deps.json";
90
+ /**
91
+ * The value `buildSite` returns. `incremental` is present only when a `changed`
92
+ * build actually rebuilt a subset of routes (absent on every full rebuild).
93
+ */
94
+ export type BuildResult = {
95
+ routes: RouteManifestEntry[];
96
+ distRoot: string;
97
+ feeds: {
98
+ sitemap: number | null;
99
+ rss: number | null;
100
+ };
101
+ incremental?: {
102
+ changed: string[];
103
+ rebuilt: string[];
104
+ total: number;
105
+ } | undefined;
106
+ };
107
+ /**
108
+ * A compiled page bound for a concrete output route. Mirrors `CompiledPage` plus
109
+ * the public `route` it's written to — for a paginated listing, page 1 keeps the
110
+ * listing route and pages 2+ get `/<route>/page/<n>/`.
111
+ */
112
+ export type EmittedPage = {
113
+ /**
114
+ * Public route path this page is written to.
115
+ */
116
+ route: string;
117
+ html: string;
118
+ assets: import("./compiler.js").Assets;
119
+ warnings: string[];
120
+ };
20
121
  export type Paths = import("./config.js").Paths;
21
122
  export type Assets = import("./compiler.js").Assets;
123
+ export type Meta = import("./compiler.js").Meta;
124
+ export type Route = import("./router.js").Route;
22
125
  /**
23
126
  * A built route entry written to `dist/routes.json`.
24
127
  */
@@ -37,3 +140,31 @@ export type RouteManifestEntry = {
37
140
  runtime: boolean;
38
141
  };
39
142
  };
143
+ /**
144
+ * One source route's record in the dev dependency map (`dist/.wd-dev-deps.json`),
145
+ * written by dev builds so a later `--changed` build can rebuild only the routes
146
+ * whose dependency graph contains the changed file.
147
+ */
148
+ export type DepMapEntry = {
149
+ /**
150
+ * Source file, project-relative, POSIX-separated.
151
+ */
152
+ file: string;
153
+ /**
154
+ * Every source file this route's compile read (the page
155
+ * itself, includes, loop JSON data files, colocated `.skin`/`.js`) —
156
+ * project-relative, POSIX-separated, sorted.
157
+ */
158
+ deps: string[];
159
+ /**
160
+ * Collection names the route loops, so a change
161
+ * to any entry of the collection rebuilds this route.
162
+ */
163
+ collections: string[];
164
+ /**
165
+ * The manifest entries this route emitted
166
+ * (one, or one per pagination page), so `routes.json`/`_headers`/the sitemap
167
+ * can be reassembled globally without recompiling untouched routes.
168
+ */
169
+ pages: RouteManifestEntry[];
170
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The structured, machine-readable catalog of the whole `.wd` authoring surface.
3
+ * @returns {DirectiveCatalog}
4
+ */
5
+ export function directiveCatalog(): DirectiveCatalog;
6
+ /**
7
+ * Render the catalog as a compact llms.txt-style markdown cheatsheet — the
8
+ * artifact an app pastes into a small model's system prompt. Generated entirely
9
+ * from {@link directiveCatalog}, so it can never disagree with what compiles.
10
+ * @returns {string}
11
+ */
12
+ export function llmsText(): string;
13
+ /** The action tokens the catalog exposes — used by the drift guard. */
14
+ export const CATALOG_ACTION_TOKENS: string[];
15
+ /**
16
+ * Whether the directive
17
+ * ships zero JS (static), needs the reactive runtime (reactive), or can be
18
+ * either depending on whether it reads `:state`/`:store` (either).
19
+ */
20
+ export type Reactivity = "static" | "reactive" | "either";
21
+ export type DirectiveEntry = {
22
+ /**
23
+ * The directive token (`@loop`, `:state`, `:::`, …).
24
+ */
25
+ name: string;
26
+ /**
27
+ * A single line, or an opener with a closer.
28
+ */
29
+ kind: "line" | "block";
30
+ /**
31
+ * One-line schematic (`[…]` = optional, `<…>` = slot).
32
+ */
33
+ syntax: string;
34
+ /**
35
+ * One-line, plain-language summary.
36
+ */
37
+ description: string;
38
+ /**
39
+ * One concrete, compilable line.
40
+ */
41
+ example: string;
42
+ reactive: Reactivity;
43
+ };
44
+ /**
45
+ * A clause / action-op / pipe / operator entry.
46
+ */
47
+ export type CatalogEntry = {
48
+ name: string;
49
+ syntax?: string | undefined;
50
+ description: string;
51
+ example?: string | undefined;
52
+ };
53
+ export type DirectiveCatalog = {
54
+ /**
55
+ * The installed Darkmown version.
56
+ */
57
+ version: string;
58
+ /**
59
+ * Always `.wd` — the file extension that gates directives.
60
+ */
61
+ format: string;
62
+ directives: DirectiveEntry[];
63
+ /**
64
+ * Clauses of the `@loop` header (fixed order).
65
+ */
66
+ loopClauses: CatalogEntry[];
67
+ /**
68
+ * Per-row meta variables valid in a loop body.
69
+ */
70
+ loopVariables: CatalogEntry[];
71
+ /**
72
+ * The `:button`/`:effect`/`:every` action vocabulary.
73
+ */
74
+ actionOps: CatalogEntry[];
75
+ /**
76
+ * The `{ value | pipe }` formatter whitelist.
77
+ */
78
+ formatPipes: CatalogEntry[];
79
+ /**
80
+ * Comparison operators for `where`/`:if`/`when`.
81
+ */
82
+ predicateOps: CatalogEntry[];
83
+ /**
84
+ * Logical joiners (`and`/`or`/`not`).
85
+ */
86
+ predicateJoiners: string[];
87
+ /**
88
+ * The `WDxxx` code blocks.
89
+ */
90
+ errorAreas: import("./errors.js").ErrorArea[];
91
+ /**
92
+ * Every stable compile-error
93
+ * code, with its cause and fix. A thrown error's message starts with its code
94
+ * (`[WD201] …`) and mirrors it on `err.wd.code`.
95
+ */
96
+ errors: import("./errors.js").ErrorEntry[];
97
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @param {string} line
3
+ * @param {Ctx} ctx
4
+ * @param {number} index 0-based line index for `file:line` errors.
5
+ * @returns {string}
6
+ */
7
+ export function handleButton(line: string, ctx: Ctx, index: number): string;
8
+ /**
9
+ * Parse `:effect <watched> -> <actions>` into a zero-output marker the runtime
10
+ * watches: when `<watched>` state changes, it runs `<actions>` (the same `:button`
11
+ * action vocabulary, `;`-chained). For arbitrary side effects beyond `:computed`
12
+ * (derive state) and fetch deps (auto-refetch).
13
+ * @param {string} line
14
+ * @param {Ctx} ctx
15
+ * @param {number} index 0-based line index for `file:line` errors.
16
+ * @returns {string}
17
+ */
18
+ export function handleEffect(line: string, ctx: Ctx, index: number): string;
19
+ /**
20
+ * Parse `:every <duration> -> <actions>` into a marker the runtime drives on a
21
+ * timer: every `<duration>` it runs `<actions>` (the same `:button` vocabulary,
22
+ * `;`-chained), and the interval auto-pauses while the tab is hidden. The one time
23
+ * primitive — behind live polling (`:every 5s -> board refetch`), clocks and
24
+ * countdowns (`:every 1s -> seconds++`), and slideshow autoplay (`:every 4s -> slide++`).
25
+ * @param {string} line
26
+ * @param {Ctx} ctx
27
+ * @param {number} index 0-based line index for `file:line` errors.
28
+ * @returns {string}
29
+ */
30
+ export function handleEvery(line: string, ctx: Ctx, index: number): string;
31
+ /**
32
+ * @typedef {import("./context.js").Ctx} Ctx
33
+ * @typedef {import("./context.js").Action} Action
34
+ */
35
+ export const BUTTON_EXAMPLE: ":button \"Add one\" -> count++";
36
+ export const EFFECT_EXAMPLE: ":effect query -> searches++";
37
+ export const EVERY_EXAMPLE: ":every 5s -> seconds++";
38
+ export const ACTION_EXAMPLE: "count++";
39
+ export const ACTION_USE: "Use: name++, name--, n += k, n -= k, name = v, flag toggle, list append/prepend v, list toggle v, list remove v, x clear, obj merge other, obj delete key, name reset — chain with \";\" — e.g. count++";
40
+ export type Ctx = import("./context.js").Ctx;
41
+ export type Action = import("./context.js").Action;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @typedef {import("./context.js").Ctx} Ctx
3
+ */
4
+ /**
5
+ * Parse a `.wd` body line-by-line into HTML, mixing directives and prose.
6
+ * @param {string[]} lines
7
+ * @param {Ctx} ctx
8
+ * @returns {string}
9
+ */
10
+ export function compileBody(lines: string[], ctx: Ctx): string;
11
+ export type Ctx = import("./context.js").Ctx;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Build the collection index from the router's discovered routes: one entry per
3
+ * `site/pages/<name>/…` route, grouped under the FIRST path segment (the
4
+ * collection name). The home route, top-level pages, and the synthetic `/404/`
5
+ * route are not entries. Reuses each route's already-parsed `meta` (no re-parse)
6
+ * and reads the `.md` body only for the excerpt fallback. When a collection has
7
+ * a `_schema.wd`, every entry is validated against it here (throwing on the
8
+ * first violation), so a malformed entry fails the build with a `file:line`.
9
+ *
10
+ * @param {Route[]} routes Post-draft-filter routes from `discoverRoutes`.
11
+ * @param {Paths} paths Resolved project paths (`routesRoot` for `_schema.wd`).
12
+ * @param {import("./reader.js").Reader} [reader] Source reader (fs-backed by
13
+ * default; pass an in-memory reader to build collections without a filesystem).
14
+ * @returns {Map<string, CollectionRow[]>} Collection name → entry rows.
15
+ */
16
+ export function buildCollections(routes: Route[], paths: Paths, reader?: import("./reader.js").Reader): Map<string, CollectionRow[]>;
17
+ /**
18
+ * A parsed `_schema.wd` field rule.
19
+ * @typedef {object} SchemaField
20
+ * @property {string} name Field name.
21
+ * @property {string} type One of {@link SCHEMA_TYPES}.
22
+ * @property {boolean} optional Whether the field may be absent (`?` modifier).
23
+ */
24
+ /**
25
+ * Read + parse a collection's `_schema.wd`, or null when the file is absent
26
+ * (validation is opt-in). The schema is frontmatter-shaped — one `field: type`
27
+ * line per rule inside the `---` block — so authors write it exactly like the
28
+ * frontmatter it validates. Each type must be in the closed {@link SCHEMA_TYPES}
29
+ * vocabulary (`?` suffix = optional); an unknown token is a compile error in the
30
+ * schema file itself, pointing at the offending line.
31
+ * @param {string} name Collection name.
32
+ * @param {Paths} paths
33
+ * @param {import("./reader.js").Reader} [reader] Source reader (fs-backed by default).
34
+ * @returns {SchemaField[] | null}
35
+ */
36
+ export function readSchema(name: string, paths: Paths, reader?: import("./reader.js").Reader): SchemaField[] | null;
37
+ /**
38
+ * Parse a `_schema.wd` source into field rules. The schema fields live in the
39
+ * frontmatter block; the body (if any) is ignored, so a schema file can carry a
40
+ * prose note after its `---` fence. Each field line is `name: type` where type
41
+ * is a closed token with an optional trailing `?`.
42
+ * @param {string} raw `_schema.wd` file contents.
43
+ * @param {string} file Absolute path, for `file:line` errors.
44
+ * @returns {SchemaField[]}
45
+ */
46
+ export function parseSchema(raw: string, file: string): SchemaField[];
47
+ /**
48
+ * A parsed `_schema.wd` field rule.
49
+ */
50
+ export type SchemaField = {
51
+ /**
52
+ * Field name.
53
+ */
54
+ name: string;
55
+ /**
56
+ * One of {@link SCHEMA_TYPES}.
57
+ */
58
+ type: string;
59
+ /**
60
+ * Whether the field may be absent (`?` modifier).
61
+ */
62
+ optional: boolean;
63
+ };
64
+ export type Paths = import("./context.js").Paths;
65
+ export type Meta = import("./context.js").Meta;
66
+ export type Route = import("../router.js").Route;
67
+ /**
68
+ * A collection entry row: every frontmatter field, plus derived `url`/`slug`/
69
+ * `excerpt`. Keyed loosely so authors' custom frontmatter keys pass through to
70
+ * `{ post.whatever }` and `where`/`sort` without a closed schema here.
71
+ */
72
+ export type CollectionRow = Record<string, unknown> & {
73
+ url: string;
74
+ slug: string;
75
+ excerpt: string;
76
+ };