@zvndev/darkmown 2.1.0 → 2.3.1-next.1.g3a4a8dc
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/CHANGELOG.md +22 -0
- package/README.md +65 -0
- package/docs/cli.md +8 -0
- package/docs/spec-alignment.md +18 -0
- package/grammar/wd-directives.gbnf +58 -0
- package/package.json +17 -10
- package/site/pages/docs/index.wd +29 -0
- package/site/pages/playground/index.js +47 -46
- package/src/builder.js +5 -0
- package/src/catalog.js +601 -0
- package/src/cli.js +8 -0
- package/src/compiler/actions.js +87 -34
- package/src/compiler/body.js +36 -14
- package/src/compiler/collections.js +16 -10
- package/src/compiler/context.js +70 -1
- package/src/compiler/fetch.js +51 -19
- package/src/compiler/format.js +5 -2
- package/src/compiler/forms.js +169 -50
- package/src/compiler/frontmatter.js +11 -2
- package/src/compiler/fs-reader.js +2 -1
- package/src/compiler/image-size.js +340 -0
- package/src/compiler/includes.js +8 -4
- package/src/compiler/interpolation.js +15 -7
- package/src/compiler/loops.js +78 -39
- package/src/compiler/markdown.js +4 -3
- package/src/compiler/media.js +38 -10
- package/src/compiler/page.js +8 -6
- package/src/compiler/predicates.js +74 -30
- package/src/compiler/reader.js +12 -2
- package/src/compiler/state.js +83 -18
- package/src/compiler/structure.js +57 -24
- package/src/deploy.js +8 -4
- package/src/errors.js +1156 -0
- package/src/grammar.js +131 -0
- package/src/index.js +1 -0
- package/src/router.js +7 -4
- package/src/scaffold.js +4 -2
- package/src/skin.js +17 -6
- package/types/api-runner.d.ts +94 -0
- package/types/builder.d.ts +141 -10
- package/types/catalog.d.ts +97 -0
- package/types/compiler/actions.d.ts +41 -0
- package/types/compiler/body.d.ts +11 -0
- package/types/compiler/collections.d.ts +76 -0
- package/types/compiler/context.d.ts +647 -0
- package/types/compiler/directives.d.ts +6 -0
- package/types/compiler/expr-ast.d.ts +20 -0
- package/types/compiler/fetch.d.ts +29 -0
- package/types/compiler/format.d.ts +49 -0
- package/types/compiler/forms.d.ts +94 -0
- package/types/compiler/frontmatter.d.ts +33 -0
- package/types/compiler/fs-reader.d.ts +7 -0
- package/types/compiler/image-size.d.ts +14 -0
- package/types/compiler/includes.d.ts +100 -0
- package/types/compiler/index.d.ts +5 -0
- package/types/compiler/interpolation.d.ts +115 -0
- package/types/compiler/loops.d.ts +36 -0
- package/types/compiler/markdown.d.ts +34 -0
- package/types/compiler/media.d.ts +32 -0
- package/types/compiler/page.d.ts +109 -0
- package/types/compiler/predicates.d.ts +50 -0
- package/types/compiler/reader.d.ts +48 -0
- package/types/compiler/state.d.ts +68 -0
- package/types/compiler/structure.d.ts +56 -0
- package/types/compiler.d.ts +35 -223
- package/types/dev.d.ts +47 -2
- package/types/errors.d.ts +100 -0
- package/types/feeds.d.ts +150 -0
- package/types/headers.d.ts +44 -0
- package/types/highlight.d.ts +24 -0
- package/types/router.d.ts +42 -3
- package/types/scaffold.d.ts +15 -3
- package/types/skin.d.ts +28 -1
- package/types/src/api-runner.d.ts +94 -0
- package/types/src/builder.d.ts +170 -0
- package/types/src/catalog.d.ts +97 -0
- package/types/src/compiler/actions.d.ts +41 -0
- package/types/src/compiler/body.d.ts +11 -0
- package/types/src/compiler/collections.d.ts +76 -0
- package/types/src/compiler/context.d.ts +647 -0
- package/types/src/compiler/directives.d.ts +6 -0
- package/types/src/compiler/expr-ast.d.ts +20 -0
- package/types/src/compiler/fetch.d.ts +29 -0
- package/types/src/compiler/format.d.ts +49 -0
- package/types/src/compiler/forms.d.ts +94 -0
- package/types/src/compiler/frontmatter.d.ts +33 -0
- package/types/src/compiler/fs-reader.d.ts +7 -0
- package/types/src/compiler/image-size.d.ts +14 -0
- package/types/src/compiler/includes.d.ts +100 -0
- package/types/src/compiler/index.d.ts +5 -0
- package/types/src/compiler/interpolation.d.ts +115 -0
- package/types/src/compiler/loops.d.ts +36 -0
- package/types/src/compiler/markdown.d.ts +34 -0
- package/types/src/compiler/media.d.ts +32 -0
- package/types/src/compiler/page.d.ts +109 -0
- package/types/src/compiler/predicates.d.ts +50 -0
- package/types/src/compiler/reader.d.ts +48 -0
- package/types/src/compiler/state.d.ts +68 -0
- package/types/src/compiler/structure.d.ts +56 -0
- package/types/src/compiler.d.ts +48 -0
- package/types/src/config.d.ts +45 -0
- package/types/src/dev.d.ts +58 -0
- package/types/src/errors.d.ts +100 -0
- package/types/src/feeds.d.ts +150 -0
- package/types/src/headers.d.ts +44 -0
- package/types/src/highlight.d.ts +24 -0
- package/types/src/router.d.ts +78 -0
- package/types/src/scaffold.d.ts +20 -0
- package/types/src/skin.d.ts +48 -0
- package/types/src/statics.d.ts +24 -0
- package/types/statics.d.ts +2 -1
package/src/builder.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { discoverApiRoutes } from "./api-runner.js";
|
|
5
|
+
import { llmsText } from "./catalog.js";
|
|
5
6
|
import { buildCollections } from "./compiler/collections.js";
|
|
6
7
|
import { compilePage, parseFrontmatter } from "./compiler.js";
|
|
7
8
|
import { createPaths, routesDir, shelfDir } from "./config.js";
|
|
@@ -364,6 +365,10 @@ function buildRoute(route, cwd, paths, collections, feedLink, warned) {
|
|
|
364
365
|
function emitGlobalFiles(manifest, paths) {
|
|
365
366
|
fs.writeFileSync(path.join(paths.distRoot, "routes.json"), JSON.stringify(manifest, null, 2));
|
|
366
367
|
fs.writeFileSync(path.join(paths.distRoot, "_headers"), renderCloudflareHeaders(manifest));
|
|
368
|
+
// `llms.txt` is the compact `.wd` cheatsheet an AI authoring tool fetches to
|
|
369
|
+
// prime a small model. It depends only on the framework version, so it re-emits
|
|
370
|
+
// identically on every build — cheap, and always present next to the site.
|
|
371
|
+
fs.writeFileSync(path.join(paths.distRoot, "llms.txt"), llmsText());
|
|
367
372
|
}
|
|
368
373
|
|
|
369
374
|
/**
|
package/src/catalog.js
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Machine-readable directive catalog + llms.txt cheatsheet.
|
|
3
|
+
//
|
|
4
|
+
// `directiveCatalog()` returns a structured description of the whole `.wd`
|
|
5
|
+
// authoring surface — every public directive, `@loop` clause, loop row variable,
|
|
6
|
+
// button-action op, format pipe, and predicate operator — with a one-line syntax
|
|
7
|
+
// template, a one-line description, one CONCRETE compilable example, and whether
|
|
8
|
+
// it needs the reactive runtime. It is the single artifact an app stuffs into a
|
|
9
|
+
// small model's system prompt (via `llmsText()`), the source the GBNF grammar
|
|
10
|
+
// generator reads, and the thing the CLI's `catalog` command prints.
|
|
11
|
+
//
|
|
12
|
+
// Examples are sourced from the SAME `*_EXAMPLE` constants the compiler's error
|
|
13
|
+
// hints use, and the operator/formatter lists are read from the compiler's own
|
|
14
|
+
// tables (FORMATTER_NAMES, PREDICATE_OPS, LOOP_META) — so the catalog cannot
|
|
15
|
+
// drift from what actually compiles (drift-guarded in tests/catalog.test.js).
|
|
16
|
+
//
|
|
17
|
+
// The catalog also carries the stable compile-error codes (`errors`, from
|
|
18
|
+
// src/errors.js): an AI edit-loop that hits `[WD201] …` can look the code up in
|
|
19
|
+
// the same artifact it learned the syntax from, without parsing the prose.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import { BUTTON_EXAMPLE, EFFECT_EXAMPLE, EVERY_EXAMPLE } from "./compiler/actions.js";
|
|
24
|
+
import { LOOP_META } from "./compiler/context.js";
|
|
25
|
+
import { FETCH_EXAMPLE } from "./compiler/fetch.js";
|
|
26
|
+
import { FORMATTER_NAMES } from "./compiler/format.js";
|
|
27
|
+
import {
|
|
28
|
+
BIND_EXAMPLE,
|
|
29
|
+
CHECKBOX_EXAMPLE,
|
|
30
|
+
FORM_EXAMPLE,
|
|
31
|
+
INPUT_EXAMPLE,
|
|
32
|
+
RADIO_EXAMPLE,
|
|
33
|
+
SELECT_EXAMPLE,
|
|
34
|
+
SLIDER_EXAMPLE,
|
|
35
|
+
SUBMIT_EXAMPLE,
|
|
36
|
+
TEXTAREA_EXAMPLE
|
|
37
|
+
} from "./compiler/forms.js";
|
|
38
|
+
import { LOOP_EXAMPLE } from "./compiler/loops.js";
|
|
39
|
+
import { AUDIO_EXAMPLE, EMBED_EXAMPLE, VIDEO_EXAMPLE } from "./compiler/media.js";
|
|
40
|
+
import { PREDICATE_JOINERS, PREDICATE_OPS } from "./compiler/predicates.js";
|
|
41
|
+
import { COMPUTED_EXAMPLE, STATE_EXAMPLE, STORE_EXAMPLE, THEME_EXAMPLE } from "./compiler/state.js";
|
|
42
|
+
import {
|
|
43
|
+
CAROUSEL_EXAMPLE,
|
|
44
|
+
CONTAINER_EXAMPLE,
|
|
45
|
+
IF_EXAMPLE,
|
|
46
|
+
INCLUDE_EXAMPLE
|
|
47
|
+
} from "./compiler/structure.js";
|
|
48
|
+
import { ERROR_AREAS, errorCatalog } from "./errors.js";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {"static" | "reactive" | "either"} Reactivity Whether the directive
|
|
52
|
+
* ships zero JS (static), needs the reactive runtime (reactive), or can be
|
|
53
|
+
* either depending on whether it reads `:state`/`:store` (either).
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {object} DirectiveEntry
|
|
58
|
+
* @property {string} name The directive token (`@loop`, `:state`, `:::`, …).
|
|
59
|
+
* @property {"line" | "block"} kind A single line, or an opener with a closer.
|
|
60
|
+
* @property {string} syntax One-line schematic (`[…]` = optional, `<…>` = slot).
|
|
61
|
+
* @property {string} description One-line, plain-language summary.
|
|
62
|
+
* @property {string} example One concrete, compilable line.
|
|
63
|
+
* @property {Reactivity} reactive
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {object} CatalogEntry A clause / action-op / pipe / operator entry.
|
|
68
|
+
* @property {string} name
|
|
69
|
+
* @property {string} [syntax]
|
|
70
|
+
* @property {string} description
|
|
71
|
+
* @property {string} [example]
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {object} DirectiveCatalog
|
|
76
|
+
* @property {string} version The installed Darkmown version.
|
|
77
|
+
* @property {string} format Always `.wd` — the file extension that gates directives.
|
|
78
|
+
* @property {DirectiveEntry[]} directives
|
|
79
|
+
* @property {CatalogEntry[]} loopClauses Clauses of the `@loop` header (fixed order).
|
|
80
|
+
* @property {CatalogEntry[]} loopVariables Per-row meta variables valid in a loop body.
|
|
81
|
+
* @property {CatalogEntry[]} actionOps The `:button`/`:effect`/`:every` action vocabulary.
|
|
82
|
+
* @property {CatalogEntry[]} formatPipes The `{ value | pipe }` formatter whitelist.
|
|
83
|
+
* @property {CatalogEntry[]} predicateOps Comparison operators for `where`/`:if`/`when`.
|
|
84
|
+
* @property {string[]} predicateJoiners Logical joiners (`and`/`or`/`not`).
|
|
85
|
+
* @property {import("./errors.js").ErrorArea[]} errorAreas The `WDxxx` code blocks.
|
|
86
|
+
* @property {import("./errors.js").ErrorEntry[]} errors Every stable compile-error
|
|
87
|
+
* code, with its cause and fix. A thrown error's message starts with its code
|
|
88
|
+
* (`[WD201] …`) and mirrors it on `err.wd.code`.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/** The directive surface. `example` is drawn from the compiler's own constants. */
|
|
92
|
+
/** @type {DirectiveEntry[]} */
|
|
93
|
+
const DIRECTIVES = [
|
|
94
|
+
{
|
|
95
|
+
name: "@include",
|
|
96
|
+
kind: "line",
|
|
97
|
+
syntax: '@include /partial.wd [with key="value" …]',
|
|
98
|
+
description:
|
|
99
|
+
"Inline another .wd/.md file from site/pages or site/_, passing optional scope args.",
|
|
100
|
+
example: INCLUDE_EXAMPLE,
|
|
101
|
+
reactive: "static"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "@loop",
|
|
105
|
+
kind: "block",
|
|
106
|
+
syntax:
|
|
107
|
+
"@loop <src> into <item> [where …] [sort by …] [reverse] [offset N] [limit N] [paginate N] [sortable] … @endloop",
|
|
108
|
+
description: "Iterate a JSON file, an in-scope value, or a :state/:store list.",
|
|
109
|
+
example: LOOP_EXAMPLE,
|
|
110
|
+
reactive: "either"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: ":::",
|
|
114
|
+
kind: "block",
|
|
115
|
+
syntax: "::: [tag] [.class …] [#id] [.class when <pred>] … :::",
|
|
116
|
+
description: "Group content in a section/div/nav/main with classes, ids, and reactive classes.",
|
|
117
|
+
example: CONTAINER_EXAMPLE,
|
|
118
|
+
reactive: "either"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: ":if",
|
|
122
|
+
kind: "block",
|
|
123
|
+
syntax: ":if <name> | :if <a> <op> <b> [and|or|not …] … [:else if …] [:else] :endif",
|
|
124
|
+
description: "Render a region only when a state/loop-item condition holds.",
|
|
125
|
+
example: IF_EXAMPLE,
|
|
126
|
+
reactive: "either"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: ":state",
|
|
130
|
+
kind: "line",
|
|
131
|
+
syntax: ":state name = value [persist]",
|
|
132
|
+
description: "Declare page-scoped reactive state (persist keeps it across reloads).",
|
|
133
|
+
example: STATE_EXAMPLE,
|
|
134
|
+
reactive: "reactive"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: ":store",
|
|
138
|
+
kind: "line",
|
|
139
|
+
syntax: ":store name = value [ephemeral]",
|
|
140
|
+
description: "Declare a page-global store, persisted by default (ephemeral opts out).",
|
|
141
|
+
example: STORE_EXAMPLE,
|
|
142
|
+
reactive: "reactive"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: ":computed",
|
|
146
|
+
kind: "line",
|
|
147
|
+
syntax: ":computed name = <expression>",
|
|
148
|
+
description: "Derive state from other state (numbers, comparisons, sum/avg/min/max/count).",
|
|
149
|
+
example: COMPUTED_EXAMPLE,
|
|
150
|
+
reactive: "reactive"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: ":fetch",
|
|
154
|
+
kind: "line",
|
|
155
|
+
syntax:
|
|
156
|
+
':fetch name from "url" [method=…] [when=visible] [timeout=ms] [retry=N] [headers=key] [body=key]',
|
|
157
|
+
description: "Load remote JSON with auto loading/error/empty lifecycle state.",
|
|
158
|
+
example: FETCH_EXAMPLE,
|
|
159
|
+
reactive: "reactive"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: ":effect",
|
|
163
|
+
kind: "line",
|
|
164
|
+
syntax: ":effect watched -> action[; action…]",
|
|
165
|
+
description: "Run button-vocabulary actions whenever the watched state changes.",
|
|
166
|
+
example: EFFECT_EXAMPLE,
|
|
167
|
+
reactive: "reactive"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: ":every",
|
|
171
|
+
kind: "line",
|
|
172
|
+
syntax: ":every <duration> -> action[; action…]",
|
|
173
|
+
description: "Run actions on a timer (5s/500ms/2m); auto-pauses while the tab is hidden.",
|
|
174
|
+
example: EVERY_EXAMPLE,
|
|
175
|
+
reactive: "reactive"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: ":theme",
|
|
179
|
+
kind: "line",
|
|
180
|
+
syntax: ':theme [name] [= "auto"]',
|
|
181
|
+
description: "Add a light/dark theme store reflected onto <html data-theme>.",
|
|
182
|
+
example: THEME_EXAMPLE,
|
|
183
|
+
reactive: "reactive"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: ":button",
|
|
187
|
+
kind: "line",
|
|
188
|
+
syntax: ':button "Label" -> action[; action…]',
|
|
189
|
+
description: "A button that runs one or more state actions on click.",
|
|
190
|
+
example: BUTTON_EXAMPLE,
|
|
191
|
+
reactive: "reactive"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: ":form",
|
|
195
|
+
kind: "block",
|
|
196
|
+
syntax: ':form [into name] [action="/url"] [method="post"] … :endform',
|
|
197
|
+
description: "A form: client state (into), native post (action), or fetch round-trip (both).",
|
|
198
|
+
example: FORM_EXAMPLE,
|
|
199
|
+
reactive: "either"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: ":input",
|
|
203
|
+
kind: "line",
|
|
204
|
+
syntax: ':input name [type=…] [placeholder="…"] [required]',
|
|
205
|
+
description: "A single-line form input (auto aria-label).",
|
|
206
|
+
example: INPUT_EXAMPLE,
|
|
207
|
+
reactive: "static"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: ":textarea",
|
|
211
|
+
kind: "line",
|
|
212
|
+
syntax: ':textarea name [placeholder="…"] [rows=N] [required]',
|
|
213
|
+
description: "A multi-line form input.",
|
|
214
|
+
example: TEXTAREA_EXAMPLE,
|
|
215
|
+
reactive: "static"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: ":select",
|
|
219
|
+
kind: "block",
|
|
220
|
+
syntax: ':select name [required] then "- Label" lines',
|
|
221
|
+
description: "A dropdown; one <option> per following - Label line.",
|
|
222
|
+
example: SELECT_EXAMPLE,
|
|
223
|
+
reactive: "static"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: ":checkbox",
|
|
227
|
+
kind: "block",
|
|
228
|
+
syntax: ':checkbox name [required] then "- Label" lines',
|
|
229
|
+
description: "A checkbox group that captures every checked value.",
|
|
230
|
+
example: CHECKBOX_EXAMPLE,
|
|
231
|
+
reactive: "static"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: ":radio",
|
|
235
|
+
kind: "block",
|
|
236
|
+
syntax: ':radio name [required] then "- Label" lines',
|
|
237
|
+
description: "A radio group that captures a single value.",
|
|
238
|
+
example: RADIO_EXAMPLE,
|
|
239
|
+
reactive: "static"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: ":submit",
|
|
243
|
+
kind: "line",
|
|
244
|
+
syntax: ':submit "Label"',
|
|
245
|
+
description: "A form submit button.",
|
|
246
|
+
example: SUBMIT_EXAMPLE,
|
|
247
|
+
reactive: "static"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: ":bind",
|
|
251
|
+
kind: "line",
|
|
252
|
+
syntax: ':bind state [placeholder="…"] [type=…]',
|
|
253
|
+
description: "A two-way text input bound to a declared :state.",
|
|
254
|
+
example: BIND_EXAMPLE,
|
|
255
|
+
reactive: "reactive"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: ":slider",
|
|
259
|
+
kind: "line",
|
|
260
|
+
syntax: ":slider name [= value] [min=N] [max=N] [step=N] [persist]",
|
|
261
|
+
description: "A range input two-way bound to a number :state.",
|
|
262
|
+
example: SLIDER_EXAMPLE,
|
|
263
|
+
reactive: "reactive"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: ":video",
|
|
267
|
+
kind: "line",
|
|
268
|
+
syntax: ":video /clip.mp4 [poster=…] [width=…] [height=…] [controls] [autoplay] [loop] [muted]",
|
|
269
|
+
description: "A hardened HTML5 <video> (zero runtime).",
|
|
270
|
+
example: VIDEO_EXAMPLE,
|
|
271
|
+
reactive: "static"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: ":audio",
|
|
275
|
+
kind: "line",
|
|
276
|
+
syntax: ":audio /clip.mp3 [preload=…] [controls] [autoplay] [loop] [muted]",
|
|
277
|
+
description: "A hardened HTML5 <audio> player (zero runtime).",
|
|
278
|
+
example: AUDIO_EXAMPLE,
|
|
279
|
+
reactive: "static"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: ":embed",
|
|
283
|
+
kind: "line",
|
|
284
|
+
syntax: ':embed <url> [title="…"]',
|
|
285
|
+
description: "A lazy, privacy-friendly YouTube/Vimeo/iframe embed.",
|
|
286
|
+
example: EMBED_EXAMPLE,
|
|
287
|
+
reactive: "static"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: ":carousel",
|
|
291
|
+
kind: "block",
|
|
292
|
+
syntax: ":carousel [autoplay=N] … :endcarousel",
|
|
293
|
+
description: "A scroll-snapping carousel; each direct child block is one slide.",
|
|
294
|
+
example: CAROUSEL_EXAMPLE,
|
|
295
|
+
reactive: "either"
|
|
296
|
+
}
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
/** The `@loop` header clauses, in the fixed order the parser accepts them. */
|
|
300
|
+
/** @type {CatalogEntry[]} */
|
|
301
|
+
const LOOP_CLAUSES = [
|
|
302
|
+
{
|
|
303
|
+
name: "where",
|
|
304
|
+
syntax: "where <item>.field <op> value [and|or …]",
|
|
305
|
+
description: "Keep only rows matching a predicate.",
|
|
306
|
+
example: "where p.price < 50"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "sort by",
|
|
310
|
+
syntax: "sort by <item>.field [asc|desc]",
|
|
311
|
+
description: "Order rows by a field (or a { state } for clickable-header sort).",
|
|
312
|
+
example: "sort by p.price asc"
|
|
313
|
+
},
|
|
314
|
+
{ name: "reverse", syntax: "reverse", description: "Reverse the row order.", example: "reverse" },
|
|
315
|
+
{
|
|
316
|
+
name: "offset",
|
|
317
|
+
syntax: "offset N",
|
|
318
|
+
description: "Skip the first N rows.",
|
|
319
|
+
example: "offset 2"
|
|
320
|
+
},
|
|
321
|
+
{ name: "limit", syntax: "limit N", description: "Keep at most N rows.", example: "limit 5" },
|
|
322
|
+
{
|
|
323
|
+
name: "paginate",
|
|
324
|
+
syntax: "paginate N",
|
|
325
|
+
description: "Split a collection listing into static pages of N.",
|
|
326
|
+
example: "paginate 10"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "sortable",
|
|
330
|
+
syntax: "sortable",
|
|
331
|
+
description: "Let the user drag-reorder a :state/:store list.",
|
|
332
|
+
example: "sortable"
|
|
333
|
+
}
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
/** Per-row meta variables valid inside a loop body, read from LOOP_META. */
|
|
337
|
+
/** @type {Record<string, string>} */
|
|
338
|
+
const LOOP_VAR_DESC = {
|
|
339
|
+
$index: "Zero-based row index.",
|
|
340
|
+
$number: "One-based row number.",
|
|
341
|
+
$first: "True on the first row.",
|
|
342
|
+
$last: "True on the last row.",
|
|
343
|
+
$count: "Total row count."
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** The button-action vocabulary. `token` is the distinguishing keyword the drift
|
|
347
|
+
* test checks against actions.js's ACTION_USE string. */
|
|
348
|
+
const ACTION_OPS = [
|
|
349
|
+
{
|
|
350
|
+
name: "increment",
|
|
351
|
+
token: "++",
|
|
352
|
+
syntax: "name++",
|
|
353
|
+
description: "Increment a number.",
|
|
354
|
+
example: "count++"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "decrement",
|
|
358
|
+
token: "--",
|
|
359
|
+
syntax: "name--",
|
|
360
|
+
description: "Decrement a number.",
|
|
361
|
+
example: "count--"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: "add",
|
|
365
|
+
token: "+=",
|
|
366
|
+
syntax: "n += k",
|
|
367
|
+
description: "Add to a number.",
|
|
368
|
+
example: "count += 5"
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "subtract",
|
|
372
|
+
token: "-=",
|
|
373
|
+
syntax: "n -= k",
|
|
374
|
+
description: "Subtract from a number.",
|
|
375
|
+
example: "count -= 5"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: "set",
|
|
379
|
+
token: "=",
|
|
380
|
+
syntax: "name = v",
|
|
381
|
+
description: "Assign a value.",
|
|
382
|
+
example: "open = true"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "toggle",
|
|
386
|
+
token: "toggle",
|
|
387
|
+
syntax: "flag toggle",
|
|
388
|
+
description: "Flip a boolean (or toggle a list member with `list toggle v`).",
|
|
389
|
+
example: "open toggle"
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "append",
|
|
393
|
+
token: "append",
|
|
394
|
+
syntax: "list append v",
|
|
395
|
+
description: "Append a value to a list.",
|
|
396
|
+
example: 'items append "x"'
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "prepend",
|
|
400
|
+
token: "prepend",
|
|
401
|
+
syntax: "list prepend v",
|
|
402
|
+
description: "Prepend a value to a list.",
|
|
403
|
+
example: 'items prepend "x"'
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "remove",
|
|
407
|
+
token: "remove",
|
|
408
|
+
syntax: "list remove v",
|
|
409
|
+
description: "Remove a value from a list (or the current loop row).",
|
|
410
|
+
example: 'items remove "x"'
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "clear",
|
|
414
|
+
token: "clear",
|
|
415
|
+
syntax: "x clear",
|
|
416
|
+
description: "Empty a list or reset a value to null.",
|
|
417
|
+
example: "items clear"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: "merge",
|
|
421
|
+
token: "merge",
|
|
422
|
+
syntax: "obj merge other",
|
|
423
|
+
description: "Shallow-merge another object/state key.",
|
|
424
|
+
example: "settings merge patch"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: "delete",
|
|
428
|
+
token: "delete",
|
|
429
|
+
syntax: "obj delete key",
|
|
430
|
+
description: "Delete a key from an object.",
|
|
431
|
+
example: 'settings delete "beta"'
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "reset",
|
|
435
|
+
token: "reset",
|
|
436
|
+
syntax: "name reset",
|
|
437
|
+
description: "Reset a state key to its initial value.",
|
|
438
|
+
example: "form reset"
|
|
439
|
+
}
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
/** Hand metadata for each formatter, keyed by name; keys are drift-checked to
|
|
443
|
+
* equal FORMATTER_NAMES exactly (tests/catalog.test.js). */
|
|
444
|
+
/** @type {Record<string, { description: string, example: string }>} */
|
|
445
|
+
const PIPE_META = {
|
|
446
|
+
money: { description: "Currency format.", example: "{ p.price | money }" },
|
|
447
|
+
number: { description: "Number with optional fixed decimals.", example: "{ p.qty | number:2 }" },
|
|
448
|
+
percent: { description: "Percentage format.", example: "{ p.rate | percent }" },
|
|
449
|
+
round: { description: "Round to N decimals.", example: "{ p.score | round:1 }" },
|
|
450
|
+
date: { description: "Localized date.", example: "{ p.published | date }" },
|
|
451
|
+
time: { description: "Localized time.", example: "{ p.published | time }" },
|
|
452
|
+
datetime: { description: "Localized date + time.", example: "{ p.published | datetime }" },
|
|
453
|
+
upper: { description: "UPPERCASE.", example: "{ p.name | upper }" },
|
|
454
|
+
lower: { description: "lowercase.", example: "{ p.name | lower }" },
|
|
455
|
+
capitalize: { description: "Capitalize the first letter.", example: "{ p.name | capitalize }" },
|
|
456
|
+
truncate: { description: "Cut to length with an ellipsis.", example: "{ p.bio | truncate:80 }" },
|
|
457
|
+
trim: { description: "Trim surrounding whitespace.", example: "{ p.name | trim }" },
|
|
458
|
+
pluralize: {
|
|
459
|
+
description: "Count + singular/plural word.",
|
|
460
|
+
example: '{ p.qty | pluralize:"item" }'
|
|
461
|
+
},
|
|
462
|
+
default: {
|
|
463
|
+
description: "Fallback when empty/null.",
|
|
464
|
+
example: '{ p.nickname | default:"friend" }'
|
|
465
|
+
},
|
|
466
|
+
sum: { description: "Sum a list (optionally a field).", example: '{ cart | sum:"price" }' },
|
|
467
|
+
avg: { description: "Average a list.", example: "{ scores | avg }" },
|
|
468
|
+
min: { description: "Minimum of a list.", example: "{ prices | min }" },
|
|
469
|
+
max: { description: "Maximum of a list.", example: "{ prices | max }" },
|
|
470
|
+
count: { description: "Item count of a list.", example: "{ cart | count }" },
|
|
471
|
+
join: { description: "Join a list into a string.", example: '{ tags | join:", " }' }
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/** One-line description per comparison operator. */
|
|
475
|
+
/** @type {Record<string, string>} */
|
|
476
|
+
const OP_DESC = {
|
|
477
|
+
contains: "List/string membership.",
|
|
478
|
+
"==": "Equal.",
|
|
479
|
+
"!=": "Not equal.",
|
|
480
|
+
">=": "Greater than or equal.",
|
|
481
|
+
"<=": "Less than or equal.",
|
|
482
|
+
">": "Greater than.",
|
|
483
|
+
"<": "Less than."
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
/** Read the installed version from package.json (always shipped beside src/). */
|
|
487
|
+
function readVersion() {
|
|
488
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
489
|
+
return String(pkg.version || "0.0.0");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The structured, machine-readable catalog of the whole `.wd` authoring surface.
|
|
494
|
+
* @returns {DirectiveCatalog}
|
|
495
|
+
*/
|
|
496
|
+
export function directiveCatalog() {
|
|
497
|
+
return {
|
|
498
|
+
version: readVersion(),
|
|
499
|
+
format: ".wd",
|
|
500
|
+
directives: DIRECTIVES.map((d) => ({ ...d })),
|
|
501
|
+
loopClauses: LOOP_CLAUSES.map((c) => ({ ...c })),
|
|
502
|
+
loopVariables: Object.keys(LOOP_META).map((name) => ({
|
|
503
|
+
name,
|
|
504
|
+
description: LOOP_VAR_DESC[name],
|
|
505
|
+
example: `{ ${name} }`
|
|
506
|
+
})),
|
|
507
|
+
actionOps: ACTION_OPS.map((a) => ({
|
|
508
|
+
name: a.name,
|
|
509
|
+
syntax: a.syntax,
|
|
510
|
+
description: a.description,
|
|
511
|
+
example: a.example
|
|
512
|
+
})),
|
|
513
|
+
formatPipes: FORMATTER_NAMES.map((name) => ({
|
|
514
|
+
name,
|
|
515
|
+
syntax: `{ value | ${name} }`,
|
|
516
|
+
description: PIPE_META[name].description,
|
|
517
|
+
example: PIPE_META[name].example
|
|
518
|
+
})),
|
|
519
|
+
predicateOps: PREDICATE_OPS.map((op) => ({
|
|
520
|
+
name: op,
|
|
521
|
+
description: OP_DESC[op],
|
|
522
|
+
example: op === "contains" ? 'p.tags contains "sale"' : `p.price ${op} 50`
|
|
523
|
+
})),
|
|
524
|
+
predicateJoiners: [...PREDICATE_JOINERS],
|
|
525
|
+
errorAreas: ERROR_AREAS.map((area) => ({ ...area })),
|
|
526
|
+
errors: errorCatalog()
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** The action tokens the catalog exposes — used by the drift guard. */
|
|
531
|
+
export const CATALOG_ACTION_TOKENS = ACTION_OPS.map((a) => a.token);
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Render the catalog as a compact llms.txt-style markdown cheatsheet — the
|
|
535
|
+
* artifact an app pastes into a small model's system prompt. Generated entirely
|
|
536
|
+
* from {@link directiveCatalog}, so it can never disagree with what compiles.
|
|
537
|
+
* @returns {string}
|
|
538
|
+
*/
|
|
539
|
+
export function llmsText() {
|
|
540
|
+
const cat = directiveCatalog();
|
|
541
|
+
const out = [];
|
|
542
|
+
out.push(`# Darkmown .wd cheatsheet (v${cat.version})`);
|
|
543
|
+
out.push("");
|
|
544
|
+
out.push("Darkmown compiles Markdown. A plain `.md` file stays plain; rename it to `.wd`");
|
|
545
|
+
out.push("to unlock the directives below. Static pages ship ZERO JavaScript; only pages");
|
|
546
|
+
out.push("that use :state/:store/:fetch/:button (reactive) load the ~8 KB runtime. Copy an");
|
|
547
|
+
out.push("example verbatim — every one compiles. Interpolate values with `{ path }`.");
|
|
548
|
+
out.push("");
|
|
549
|
+
|
|
550
|
+
out.push("## Directives");
|
|
551
|
+
for (const d of cat.directives) {
|
|
552
|
+
out.push(`- \`${d.name}\` (${d.reactive}) — ${d.description} e.g. \`${d.example}\``);
|
|
553
|
+
}
|
|
554
|
+
out.push("");
|
|
555
|
+
|
|
556
|
+
out.push("## @loop clauses (this fixed order, all optional)");
|
|
557
|
+
for (const c of cat.loopClauses) {
|
|
558
|
+
out.push(`- \`${c.syntax}\` — ${c.description} e.g. \`${c.example}\``);
|
|
559
|
+
}
|
|
560
|
+
out.push("");
|
|
561
|
+
|
|
562
|
+
out.push("## Loop row variables (inside a loop body)");
|
|
563
|
+
out.push(
|
|
564
|
+
`- ${cat.loopVariables.map((v) => `\`${v.name}\``).join(", ")} — ${cat.loopVariables
|
|
565
|
+
.map((v) => `${v.name} = ${v.description.toLowerCase().replace(/\.$/, "")}`)
|
|
566
|
+
.join("; ")}.`
|
|
567
|
+
);
|
|
568
|
+
out.push("");
|
|
569
|
+
|
|
570
|
+
out.push("## Button / :effect / :every actions (chain with `;`)");
|
|
571
|
+
for (const a of cat.actionOps) {
|
|
572
|
+
out.push(`- \`${a.syntax}\` — ${a.description} e.g. \`${a.example}\``);
|
|
573
|
+
}
|
|
574
|
+
out.push("");
|
|
575
|
+
|
|
576
|
+
out.push("## Format pipes `{ value | name:arg }`");
|
|
577
|
+
for (const p of cat.formatPipes) {
|
|
578
|
+
out.push(`- \`${p.name}\` — ${p.description} e.g. \`${p.example}\``);
|
|
579
|
+
}
|
|
580
|
+
out.push("");
|
|
581
|
+
|
|
582
|
+
out.push("## Predicate operators (where / :if / `.class when`)");
|
|
583
|
+
out.push(
|
|
584
|
+
`- ${cat.predicateOps.map((o) => `\`${o.name}\``).join(", ")} — join conditions with ${cat.predicateJoiners
|
|
585
|
+
.map((j) => `\`${j}\``)
|
|
586
|
+
.join(" / ")}. e.g. \`${cat.predicateOps[0].example}\``
|
|
587
|
+
);
|
|
588
|
+
out.push("");
|
|
589
|
+
|
|
590
|
+
out.push("## Rules");
|
|
591
|
+
out.push("- `.md` never gets directives — the `.wd` extension is the feature gate.");
|
|
592
|
+
out.push("- One loop syntax (`@loop … into … @endloop`), one interpolation syntax (`{ name }`).");
|
|
593
|
+
out.push("- Actions and predicates are a fixed whitelist — no JavaScript, no `x.filter(...)`.");
|
|
594
|
+
out.push("- Close every block: `@endloop`, `:endif`, `:endform`, `:endcarousel`, `:::`.");
|
|
595
|
+
out.push(
|
|
596
|
+
"- Compile errors start with a stable code (`[WD201] …`) and always name the file," +
|
|
597
|
+
" the line, and a fix. Read the message; it tells you exactly what to write."
|
|
598
|
+
);
|
|
599
|
+
out.push("");
|
|
600
|
+
return out.join("\n");
|
|
601
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { handleApiRequest } from "./api-runner.js";
|
|
8
8
|
import { buildSite } from "./builder.js";
|
|
9
|
+
import { directiveCatalog, llmsText } from "./catalog.js";
|
|
9
10
|
import { createPaths } from "./config.js";
|
|
10
11
|
import { DEPLOY_TARGETS, deploy } from "./deploy.js";
|
|
11
12
|
import {
|
|
@@ -62,6 +63,12 @@ export async function run(argv, env = {}) {
|
|
|
62
63
|
log(pkg.version);
|
|
63
64
|
return { command: "version" };
|
|
64
65
|
}
|
|
66
|
+
if (command === "catalog") {
|
|
67
|
+
// `catalog` prints the machine-readable directive catalog as JSON; `--llms`
|
|
68
|
+
// prints the compact llms.txt cheatsheet an app pastes into a model prompt.
|
|
69
|
+
log(argv.includes("--llms") ? llmsText() : JSON.stringify(directiveCatalog(), null, 2));
|
|
70
|
+
return { command: "catalog" };
|
|
71
|
+
}
|
|
65
72
|
if (command === "init") {
|
|
66
73
|
const target = argv.slice(1).find((arg) => !arg.startsWith("-")) || ".";
|
|
67
74
|
const template = flagValue(argv, "--template");
|
|
@@ -385,6 +392,7 @@ Usage:
|
|
|
385
392
|
[--drafts] Include draft: true pages (staging)
|
|
386
393
|
darkmown deploy <${DEPLOY_TARGETS.join("|")}> [--prod] Build + deploy to a platform
|
|
387
394
|
darkmown serve Preview the built dist locally
|
|
395
|
+
darkmown catalog [--llms] Print the .wd directive catalog (JSON, or --llms cheatsheet)
|
|
388
396
|
darkmown version Print the installed Darkmown version
|
|
389
397
|
darkmown help Show this help
|
|
390
398
|
|