@zvndev/darkmown 0.19.0 → 1.0.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 (69) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +115 -5
  4. package/docs/spec-alignment.md +16 -3
  5. package/package.json +22 -4
  6. package/site/_/base.skin +372 -0
  7. package/site/_/base.wd +4 -0
  8. package/site/_/footer.wd +14 -0
  9. package/site/_/metrics.json +13 -0
  10. package/site/_/nav.skin +35 -12
  11. package/site/_/nav.wd +8 -10
  12. package/site/_/products.json +10 -0
  13. package/site/pages/app.wd +1 -1
  14. package/site/pages/carousel/index.js +82 -0
  15. package/site/pages/carousel/index.skin +185 -0
  16. package/site/pages/carousel/index.wd +109 -0
  17. package/site/pages/carousel/slide-1.jpg +0 -0
  18. package/site/pages/carousel/slide-2.jpg +0 -0
  19. package/site/pages/carousel/slide-3.jpg +0 -0
  20. package/site/pages/carousel/slide-4.jpg +0 -0
  21. package/site/pages/carousel/slide-5.jpg +0 -0
  22. package/site/pages/compass/index.skin +247 -0
  23. package/site/pages/compass/index.wd +194 -0
  24. package/site/pages/docs/index.skin +48 -28
  25. package/site/pages/docs/index.wd +71 -5
  26. package/site/pages/folio/-style.skin +375 -0
  27. package/site/pages/folio/-style.wd +6 -0
  28. package/site/pages/folio/cart.skin +309 -0
  29. package/site/pages/folio/cart.wd +201 -0
  30. package/site/pages/folio/index.skin +24 -0
  31. package/site/pages/folio/index.wd +167 -0
  32. package/site/pages/forge/index.skin +676 -0
  33. package/site/pages/forge/index.wd +330 -0
  34. package/site/pages/index.skin +135 -144
  35. package/site/pages/index.wd +17 -5
  36. package/site/pages/ledger/index.skin +257 -0
  37. package/site/pages/ledger/index.wd +174 -0
  38. package/site/pages/media/clip-poster.jpg +0 -0
  39. package/site/pages/media/clip.mp4 +0 -0
  40. package/site/pages/media/index.skin +82 -0
  41. package/site/pages/media/index.wd +84 -0
  42. package/site/pages/media/tone.mp3 +0 -0
  43. package/site/pages/pulse/index.skin +434 -0
  44. package/site/pages/pulse/index.wd +178 -0
  45. package/site/pages/showcase/index.skin +130 -0
  46. package/site/pages/showcase/index.wd +168 -0
  47. package/src/builder.js +20 -14
  48. package/src/cli.js +216 -77
  49. package/src/compiler/body.js +457 -0
  50. package/src/compiler/context.js +160 -0
  51. package/src/compiler/directives.js +1345 -0
  52. package/src/compiler/format.js +244 -0
  53. package/src/compiler/frontmatter.js +117 -0
  54. package/src/compiler/includes.js +116 -0
  55. package/src/compiler/index.js +22 -0
  56. package/src/compiler/interpolation.js +223 -0
  57. package/src/compiler/loops.js +715 -0
  58. package/src/compiler/markdown.js +242 -0
  59. package/src/compiler/page.js +267 -0
  60. package/src/compiler/predicates.js +314 -0
  61. package/src/compiler.js +26 -2932
  62. package/src/dev.js +3 -1
  63. package/src/headers.js +8 -4
  64. package/src/index.js +22 -0
  65. package/src/router.js +3 -1
  66. package/src/runtime.js +168 -14
  67. package/src/scaffold.js +158 -131
  68. package/src/skin.js +11 -2
  69. package/site/_/og.png +0 -0
@@ -0,0 +1,457 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Block parser: directives + prose segments. `compileBody` walks a `.wd` body
3
+ // line-by-line, honoring fenced code, dispatching each directive opener to its
4
+ // handler (passing the 0-based line index so malformed errors report file:line)
5
+ // and flushing intervening prose through the markdown layer. The block scanners
6
+ // (`scanBlock`/`scanContainer`/`scanConditional`/`splitEmptyBranch`) capture
7
+ // multi-line directive bodies with nesting + fence awareness.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ import { at } from "./context.js";
11
+ import {
12
+ handleBind,
13
+ handleButton,
14
+ handleChoiceGroup,
15
+ handleComputed,
16
+ handleContainer,
17
+ handleEffect,
18
+ handleEmbed,
19
+ handleEvery,
20
+ handleFetch,
21
+ handleForm,
22
+ handleIf,
23
+ handleInclude,
24
+ handleInput,
25
+ handleMedia,
26
+ handleSelect,
27
+ handleState,
28
+ handleStore,
29
+ handleSubmit,
30
+ handleTextarea,
31
+ handleTheme,
32
+ renderDemoDirective
33
+ } from "./directives.js";
34
+ import { handleLoop } from "./loops.js";
35
+ import { renderProse } from "./markdown.js";
36
+
37
+ /**
38
+ * @typedef {import("./context.js").Ctx} Ctx
39
+ */
40
+
41
+ /**
42
+ * Parse a `.wd` body line-by-line into HTML, mixing directives and prose.
43
+ * @param {string[]} lines
44
+ * @param {Ctx} ctx
45
+ * @returns {string}
46
+ */
47
+ export function compileBody(lines, ctx) {
48
+ /** @type {string[]} */
49
+ const out = [];
50
+ /** @type {string[]} */
51
+ let prose = [];
52
+ let fence = null;
53
+
54
+ const flush = () => {
55
+ if (!prose.length) return;
56
+ const text = prose.join("\n");
57
+ prose = [];
58
+ if (text.trim()) out.push(renderProse(text, ctx));
59
+ };
60
+
61
+ for (let i = 0; i < lines.length; i++) {
62
+ const line = lines[i];
63
+ const fenceMatch = line.match(/^(```+|~~~+)/);
64
+ if (fence) {
65
+ prose.push(line);
66
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length)
67
+ fence = null;
68
+ continue;
69
+ }
70
+ if (fenceMatch) {
71
+ fence = fenceMatch[1];
72
+ prose.push(line);
73
+ continue;
74
+ }
75
+
76
+ if (/^@include\s/.test(line)) {
77
+ flush();
78
+ out.push(handleInclude(line, ctx, i));
79
+ continue;
80
+ }
81
+ if (/^@loop\s/.test(line)) {
82
+ flush();
83
+ const block = scanBlock(lines, i, /^@loop\s/, "@endloop", ctx.file);
84
+ const split = splitEmptyBranch(block.body);
85
+ out.push(handleLoop(line, split.body, split.empty, ctx));
86
+ i = block.end;
87
+ continue;
88
+ }
89
+ const container = line.match(/^:::\s*(.*)$/);
90
+ if (container) {
91
+ flush();
92
+ if (!container[1].trim())
93
+ throw new Error(`Stray ::: close with no open container in ${ctx.file}`);
94
+ const block = scanContainer(lines, i, ctx.file);
95
+ out.push(handleContainer(container[1].trim(), block.body, ctx, i));
96
+ i = block.end;
97
+ continue;
98
+ }
99
+ if (/^:state\s/.test(line)) {
100
+ flush();
101
+ out.push(handleState(line, ctx, i));
102
+ continue;
103
+ }
104
+ if (/^:store\s/.test(line)) {
105
+ flush();
106
+ out.push(handleStore(line, ctx, i));
107
+ continue;
108
+ }
109
+ if (/^:fetch\s/.test(line)) {
110
+ flush();
111
+ out.push(handleFetch(line, ctx, i));
112
+ continue;
113
+ }
114
+ if (/^:computed\s/.test(line)) {
115
+ flush();
116
+ out.push(handleComputed(line, ctx, i));
117
+ continue;
118
+ }
119
+ if (/^:effect\s/.test(line)) {
120
+ flush();
121
+ out.push(handleEffect(line, ctx, i));
122
+ continue;
123
+ }
124
+ if (/^:every\s/.test(line)) {
125
+ flush();
126
+ out.push(handleEvery(line, ctx, i));
127
+ continue;
128
+ }
129
+ if (/^:theme(?:\s|$)/.test(line)) {
130
+ flush();
131
+ out.push(handleTheme(line, ctx, i));
132
+ continue;
133
+ }
134
+ const media = line.match(/^:(video|audio)\s/);
135
+ if (media) {
136
+ flush();
137
+ out.push(handleMedia(line, /** @type {"video" | "audio"} */ (media[1]), ctx, i));
138
+ continue;
139
+ }
140
+ if (/^:embed\s/.test(line)) {
141
+ flush();
142
+ out.push(handleEmbed(line, ctx, i));
143
+ continue;
144
+ }
145
+ if (/^:form\s/.test(line)) {
146
+ flush();
147
+ const block = scanBlock(lines, i, /^:form\s/, ":endform", ctx.file);
148
+ out.push(handleForm(line, block.body, ctx, i));
149
+ i = block.end;
150
+ continue;
151
+ }
152
+ if (/^:input\s/.test(line)) {
153
+ flush();
154
+ out.push(handleInput(line, ctx, i));
155
+ continue;
156
+ }
157
+ if (/^:textarea\s/.test(line)) {
158
+ flush();
159
+ out.push(handleTextarea(line, ctx, i));
160
+ continue;
161
+ }
162
+ if (/^:select\s/.test(line)) {
163
+ flush();
164
+ const opts = [];
165
+ let j = i + 1;
166
+ while (j < lines.length && /^\s*-\s+/.test(lines[j])) {
167
+ opts.push(lines[j]);
168
+ j++;
169
+ }
170
+ out.push(handleSelect(line, opts, ctx, i));
171
+ i = j - 1;
172
+ continue;
173
+ }
174
+ const choiceMatch = line.match(/^:(checkbox|radio)\s/);
175
+ if (choiceMatch) {
176
+ flush();
177
+ const opts = [];
178
+ let j = i + 1;
179
+ while (j < lines.length && /^\s*-\s+/.test(lines[j])) {
180
+ opts.push(lines[j]);
181
+ j++;
182
+ }
183
+ out.push(
184
+ handleChoiceGroup(line, opts, ctx, /** @type {"checkbox" | "radio"} */ (choiceMatch[1]), i)
185
+ );
186
+ i = j - 1;
187
+ continue;
188
+ }
189
+ if (/^:bind\s/.test(line)) {
190
+ flush();
191
+ out.push(handleBind(line, ctx, i));
192
+ continue;
193
+ }
194
+ if (/^:submit\s/.test(line)) {
195
+ flush();
196
+ out.push(handleSubmit(line, ctx, i));
197
+ continue;
198
+ }
199
+ if (/^:button\s/.test(line)) {
200
+ flush();
201
+ out.push(handleButton(line, ctx, i));
202
+ continue;
203
+ }
204
+ if (/^:if\s/.test(line)) {
205
+ flush();
206
+ const block = scanConditional(lines, i, ctx.file);
207
+ out.push(handleIf(line, block.truthy, block.falsy, ctx, i));
208
+ i = block.end;
209
+ continue;
210
+ }
211
+ const demo = renderDemoDirective(line, ctx);
212
+ if (demo) {
213
+ flush();
214
+ out.push(demo);
215
+ continue;
216
+ }
217
+ if (/^@repeat\b/.test(line)) {
218
+ throw new Error(
219
+ `@repeat was replaced by @loop in ${ctx.file}. Use: @loop /data.json into item ... @endloop`
220
+ );
221
+ }
222
+ if (/^:for\b/.test(line)) {
223
+ throw new Error(
224
+ `:for was replaced by @loop in ${ctx.file}. Use: @loop items into item ... @endloop`
225
+ );
226
+ }
227
+ if (/^(@endloop|:endif|:endfor|:endform|:else)\s*$/.test(line)) {
228
+ throw new Error(`Stray "${line.trim()}" with no matching opener in ${ctx.file}`);
229
+ }
230
+ warnUnknownDirective(line, ctx);
231
+ prose.push(line);
232
+ }
233
+
234
+ flush();
235
+ return out.join("\n");
236
+ }
237
+
238
+ // Lines that reached prose but LOOK like a directive (a `@word`/`:word` token at
239
+ // the very start, followed by a space or end of line) and match no handler are
240
+ // almost always a typo — every real directive was intercepted above. Warn, never
241
+ // throw: prose legitimately contains `@` and `:`, so the match is deliberately
242
+ // narrow (lowercase token + boundary; emoji shortcodes, times, and `@user`
243
+ // followed by punctuation do not match).
244
+ const KNOWN_DIRECTIVE =
245
+ /^(?:@(?:include|loop|empty|endloop)|:(?:state|store|fetch|computed|effect|every|theme|video|audio|embed|form|endform|input|textarea|select|checkbox|radio|bind|submit|button|if|endif|else|try|note|sprint))(?:\s|$)/;
246
+ /**
247
+ * @param {string} line
248
+ * @param {Ctx} ctx
249
+ * @returns {void}
250
+ */
251
+ function warnUnknownDirective(line, ctx) {
252
+ // Catch capitalized (`:State`) and hyphenated (`:end-loop`) typos too — the
253
+ // known-directive whitelist stays case-sensitive, so a mis-cased real
254
+ // directive correctly trips the warning. Still unindented-only, to keep prose
255
+ // that legitimately starts lines with `@`/`:` from false-positiving.
256
+ const m = line.match(/^([@:][A-Za-z][A-Za-z-]*)(?:\s|$)/);
257
+ if (m && !KNOWN_DIRECTIVE.test(line)) {
258
+ ctx.comp.warnings.push(
259
+ `${ctx.file}: "${m[1]}" looks like a directive but matches none — it will render as literal text. Check the spelling (e.g. @loop, @include, :state, :if).`
260
+ );
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Capture lines from an opener until a matching close token, honoring nesting and fences.
266
+ * @param {string[]} lines
267
+ * @param {number} start Index of the opening line.
268
+ * @param {RegExp} openRe Pattern that re-opens a nested block.
269
+ * @param {string} endToken Literal closing token (e.g. `@endloop`).
270
+ * @param {string} file Source path for errors.
271
+ * @returns {{ body: string[], end: number }}
272
+ */
273
+ function scanBlock(lines, start, openRe, endToken, file) {
274
+ /** @type {string[]} */
275
+ const body = [];
276
+ let depth = 0;
277
+ let fence = null;
278
+ for (let i = start + 1; i < lines.length; i++) {
279
+ const line = lines[i];
280
+ const fenceMatch = line.match(/^(```+|~~~+)/);
281
+ if (fence) {
282
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length)
283
+ fence = null;
284
+ body.push(line);
285
+ continue;
286
+ }
287
+ if (fenceMatch) {
288
+ fence = fenceMatch[1];
289
+ body.push(line);
290
+ continue;
291
+ }
292
+ if (openRe.test(line)) depth++;
293
+ if (line.trim() === endToken) {
294
+ if (depth === 0) return { body, end: i };
295
+ depth--;
296
+ }
297
+ body.push(line);
298
+ }
299
+ throw new Error(`Missing ${endToken} for "${lines[start]}" in ${at(file, start)}`);
300
+ }
301
+
302
+ /**
303
+ * Split a `@loop` body at a top-level `@empty` into the rows body and the empty
304
+ * branch. Honors nested `@loop … @endloop` (so an inner loop's `@empty` is not
305
+ * mistaken for the outer one) and fenced code.
306
+ * @param {string[]} body
307
+ * @returns {{ body: string[], empty: string[] | null }}
308
+ */
309
+ function splitEmptyBranch(body) {
310
+ /** @type {string[]} */
311
+ const rows = [];
312
+ /** @type {string[] | null} */
313
+ let empty = null;
314
+ let target = rows;
315
+ let depth = 0;
316
+ let fence = null;
317
+ for (const line of body) {
318
+ const fenceMatch = line.match(/^(```+|~~~+)/);
319
+ if (fence) {
320
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length)
321
+ fence = null;
322
+ target.push(line);
323
+ continue;
324
+ }
325
+ if (fenceMatch) {
326
+ fence = fenceMatch[1];
327
+ target.push(line);
328
+ continue;
329
+ }
330
+ if (/^@loop\s/.test(line)) depth++;
331
+ if (line.trim() === "@endloop") depth--;
332
+ if (depth === 0 && line.trim() === "@empty") {
333
+ empty = [];
334
+ target = empty;
335
+ continue;
336
+ }
337
+ target.push(line);
338
+ }
339
+ return { body: rows, empty };
340
+ }
341
+
342
+ /**
343
+ * Capture the body of a `:::` container up to its matching close, honoring nesting.
344
+ * @param {string[]} lines
345
+ * @param {number} start Index of the opening line.
346
+ * @param {string} file Source path for errors.
347
+ * @returns {{ body: string[], end: number }}
348
+ */
349
+ function scanContainer(lines, start, file) {
350
+ /** @type {string[]} */
351
+ const body = [];
352
+ let depth = 0;
353
+ let fence = null;
354
+ for (let i = start + 1; i < lines.length; i++) {
355
+ const line = lines[i];
356
+ const fenceMatch = line.match(/^(```+|~~~+)/);
357
+ if (fence) {
358
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length)
359
+ fence = null;
360
+ body.push(line);
361
+ continue;
362
+ }
363
+ if (fenceMatch) {
364
+ fence = fenceMatch[1];
365
+ body.push(line);
366
+ continue;
367
+ }
368
+ const marker = line.match(/^:::\s*(.*)$/);
369
+ if (marker) {
370
+ if (marker[1].trim()) {
371
+ depth++;
372
+ } else if (depth === 0) {
373
+ return { body, end: i };
374
+ } else {
375
+ depth--;
376
+ }
377
+ }
378
+ body.push(line);
379
+ }
380
+ throw new Error(`Missing closing ::: for "${lines[start]}" in ${at(file, start)}`);
381
+ }
382
+
383
+ /**
384
+ * Split an `:if … :else if … :else … :endif` chain into the first condition's
385
+ * truthy body and a falsy body. A bare `:else` works as before. The first
386
+ * `:else if B` is **desugared** into a nested `:if` that lives in the falsy
387
+ * branch: the falsy body becomes `[":if B", …rest…, ":endif"]`, which re-enters
388
+ * `compileBody`/`handleIf` recursively. A whole `:if/:else if/:else` chain
389
+ * therefore compiles to nested `data-wd-if` regions the runtime already drives —
390
+ * identical behavior for static, reactive, and loop-row conditionals.
391
+ * @param {string[]} lines
392
+ * @param {number} start Index of the `:if` line.
393
+ * @param {string} file Source path for errors.
394
+ * @returns {{ truthy: string[], falsy: string[], end: number }}
395
+ */
396
+ function scanConditional(lines, start, file) {
397
+ /** @type {string[]} */
398
+ const truthy = [];
399
+ /** @type {string[]} */
400
+ const falsy = [];
401
+ let current = truthy;
402
+ // "truthy": collecting the :if body. "else": a bare :else closed the chain —
403
+ // no further branches allowed. "elseif": the first :else if was desugared into
404
+ // a nested :if inside `falsy`; subsequent depth-0 :else if/:else lines belong to
405
+ // that nested chain and are emitted verbatim for the recursive compile to split.
406
+ let mode = "truthy";
407
+ let depth = 0;
408
+ let fence = null;
409
+ for (let i = start + 1; i < lines.length; i++) {
410
+ const line = lines[i];
411
+ const fenceMatch = line.match(/^(```+|~~~+)/);
412
+ if (fence) {
413
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length)
414
+ fence = null;
415
+ current.push(line);
416
+ continue;
417
+ }
418
+ if (fenceMatch) {
419
+ fence = fenceMatch[1];
420
+ current.push(line);
421
+ continue;
422
+ }
423
+ if (/^:if\s/.test(line)) depth++;
424
+ if (line.trim() === ":endif") {
425
+ if (depth === 0) {
426
+ if (mode === "elseif") falsy.push(":endif"); // close the synthesized nested :if
427
+ return { truthy, falsy, end: i };
428
+ }
429
+ depth--;
430
+ }
431
+ if (depth === 0 && mode !== "elseif") {
432
+ const t = line.trim();
433
+ const elseIf = t.match(/^:else if\s+(.+?)\s*$/);
434
+ if (elseIf) {
435
+ if (mode === "else")
436
+ throw new Error(
437
+ `":else if" after ":else" in ${file}. ":else" must be the last branch — order the "else if" conditions before the bare ":else".`
438
+ );
439
+ falsy.push(`:if ${elseIf[1]}`); // desugar the chain tail into a nested :if
440
+ current = falsy;
441
+ mode = "elseif";
442
+ continue;
443
+ }
444
+ if (t === ":else") {
445
+ if (mode === "else")
446
+ throw new Error(
447
+ `Duplicate ":else" in ${file}. A conditional may have only one bare ":else".`
448
+ );
449
+ current = falsy;
450
+ mode = "else";
451
+ continue;
452
+ }
453
+ }
454
+ current.push(line);
455
+ }
456
+ throw new Error(`Missing :endif for "${lines[start]}" in ${at(file, start)}`);
457
+ }
@@ -0,0 +1,160 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared types + tiny helpers threaded through every compiler module.
3
+ //
4
+ // The compiler is split along its natural seams (frontmatter, body parser,
5
+ // directive handlers, loop pipeline, predicates, interpolation, includes,
6
+ // markdown, page shell). This module holds the typedefs and the small helpers
7
+ // that the rest of the modules lean on, so the dependency graph stays a clean
8
+ // DAG with this file at the root (it imports nothing of its own).
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /**
12
+ * @typedef {import("../config.js").Paths} Paths
13
+ */
14
+
15
+ /**
16
+ * Frontmatter values are scalars or inline arrays of scalars.
17
+ * @typedef {string | number | boolean | null | Array<string | number | boolean | null>} FrontmatterValue
18
+ */
19
+
20
+ /**
21
+ * Parsed page frontmatter, keyed by field name.
22
+ * @typedef {Record<string, FrontmatterValue>} Meta
23
+ */
24
+
25
+ /**
26
+ * Collected output assets for a compiled document.
27
+ * @typedef {object} Assets
28
+ * @property {Set<string>} skins Public hrefs of compiled skin stylesheets.
29
+ * @property {Set<string>} scripts Public hrefs of colocated page scripts.
30
+ * @property {Map<string, string>} files Source path → public href for emitted assets.
31
+ * @property {boolean} runtime Whether the reactive runtime is required.
32
+ */
33
+
34
+ /**
35
+ * Per-document compilation accumulator shared across includes/sections.
36
+ * @typedef {object} Compilation
37
+ * @property {Assets} assets
38
+ * @property {Map<string, unknown>} state Declared state keys → initial values.
39
+ * @property {Set<string>} stores Page-global store names (a subset of state keys).
40
+ * @property {string[]} warnings Non-fatal authoring hints.
41
+ * @property {number} sectionCounter Counter for auto-generated section ids.
42
+ */
43
+
44
+ /**
45
+ * A lexical scope chain for static interpolation values (include args, loop vars).
46
+ * @typedef {object} Scope
47
+ * @property {Scope | null} parent
48
+ * @property {Record<string, unknown>} vars
49
+ */
50
+
51
+ /**
52
+ * Per-file compile context threaded through the directive handlers.
53
+ * @typedef {object} Ctx
54
+ * @property {string} file Absolute path to the file being compiled.
55
+ * @property {Paths} context Resolved project paths.
56
+ * @property {string[]} stack Include stack (real paths) for cycle detection.
57
+ * @property {Scope} scope Static value scope chain.
58
+ * @property {Compilation} comp Shared compilation accumulator.
59
+ * @property {string[]} sections Active section-id scope chain.
60
+ * @property {string | null} loopItem Name of the current reactive loop item, if any.
61
+ * @property {string} [loopKey] State key of the list being looped, if any.
62
+ * @property {boolean} [loopMeta] Inside a loop body, so `$index`/`$first`/… are valid.
63
+ * @property {import("markdown-it").default} [md] Markdown-it instance selected for this file.
64
+ */
65
+
66
+ /**
67
+ * A compiled page document.
68
+ * @typedef {object} CompiledDocument
69
+ * @property {Meta} meta
70
+ * @property {string} html
71
+ * @property {Assets} assets
72
+ * @property {string[]} warnings
73
+ */
74
+
75
+ /**
76
+ * A compiled page: the document plus its full HTML shell.
77
+ * @typedef {object} CompiledPage
78
+ * @property {Meta} meta
79
+ * @property {string} html
80
+ * @property {Assets} assets
81
+ * @property {string[]} warnings
82
+ */
83
+
84
+ /**
85
+ * A compiled `@loop … where` predicate.
86
+ * @typedef {object} Predicate
87
+ * @property {string} body Safe JS boolean expression over `I()`/`S()`/`C()`.
88
+ * @property {boolean} refsState Whether the predicate reads declared state.
89
+ */
90
+
91
+ /**
92
+ * A loop offset/limit argument: a non-negative integer literal or a state key.
93
+ * @typedef {{ kind: "literal", value: number } | { kind: "key", value: string }} NumArg
94
+ */
95
+
96
+ /**
97
+ * Loop clause configuration shared by the static and reactive paths.
98
+ * @typedef {object} LoopOpts
99
+ * @property {Predicate | null} where
100
+ * @property {{ key: string, keyKind: "literal" | "key", dir: string, dirKind: "literal" | "key" } | null} sort A literal item field + asc/desc, or a `:state`/`:store` key for either (reactive, clickable-header sort).
101
+ * @property {boolean} reverse
102
+ * @property {NumArg | null} offset
103
+ * @property {NumArg | null} limit
104
+ * @property {string[] | null} empty Empty-branch body lines, if any.
105
+ * @property {boolean} clauseRefsState Whether offset/limit reference state.
106
+ */
107
+
108
+ /**
109
+ * A parsed `:button` action.
110
+ * @typedef {object} Action
111
+ * @property {string} op Runtime operation (inc/dec/add/sub/append/prepend/set/toggle/member-toggle/remove/remove-value/clear/merge/delete/reset/append-row).
112
+ * @property {string} target State key (possibly dotted) the action mutates.
113
+ * @property {unknown} [value] Literal value for value-carrying ops.
114
+ */
115
+
116
+ // Page-include extensions are the only two file types `@include` will pull in.
117
+ export const pageIncludeExtensions = [".md", ".wd"];
118
+
119
+ // Reserved per-row meta variables, mapped to their runtime marker token. Shared
120
+ // by the body parser, the `:if` handler, and prose interpolation.
121
+ /** @type {Record<string, string>} */
122
+ export const LOOP_META = {
123
+ $index: "index",
124
+ $number: "number",
125
+ $first: "first",
126
+ $last: "last",
127
+ $count: "count"
128
+ };
129
+
130
+ /** @returns {Compilation} */
131
+ export function createCompilation() {
132
+ return {
133
+ assets: { skins: new Set(), scripts: new Set(), files: new Map(), runtime: false },
134
+ state: new Map(),
135
+ stores: new Set(),
136
+ warnings: [],
137
+ sectionCounter: 0
138
+ };
139
+ }
140
+
141
+ /**
142
+ * @param {Scope | null} parent
143
+ * @param {Record<string, unknown>} [vars]
144
+ * @returns {Scope}
145
+ */
146
+ export function createScope(parent, vars = {}) {
147
+ return { parent, vars: { ...vars } };
148
+ }
149
+
150
+ /**
151
+ * Format a source location as `file:line` (1-based) for compile errors. Keeps the
152
+ * file path intact so existing message matchers still pass, while pointing at the
153
+ * directive's (or unclosed opener's) line.
154
+ * @param {string} file
155
+ * @param {number} index 0-based line index.
156
+ * @returns {string}
157
+ */
158
+ export function at(file, index) {
159
+ return `${file}:${index + 1}`;
160
+ }