@zvndev/darkmown 1.5.0 → 2.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 (88) hide show
  1. package/AGENTS.md +17 -3
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +44 -16
  4. package/SECURITY.md +21 -18
  5. package/docs/cli.md +5 -1
  6. package/docs/spec-alignment.md +19 -6
  7. package/package.json +1 -1
  8. package/site/_/base.skin +8 -0
  9. package/site/_/base.wd +4 -0
  10. package/site/_/feature-card.wd +4 -0
  11. package/site/_/footer.wd +4 -0
  12. package/site/_/nav.wd +4 -0
  13. package/site/_/scoped-promo.wd +4 -0
  14. package/site/_/scoped-stat.wd +4 -0
  15. package/site/pages/404.wd +1 -0
  16. package/site/pages/app.wd +1 -0
  17. package/site/pages/blog/collections-and-pagination.skin +127 -0
  18. package/site/pages/blog/drafts-that-never-leak.skin +127 -0
  19. package/site/pages/blog/index.wd +1 -0
  20. package/site/pages/blog/shipping-feeds.skin +127 -0
  21. package/site/pages/blog/typed-frontmatter.skin +127 -0
  22. package/site/pages/blog/unfinished-thoughts.skin +127 -0
  23. package/site/pages/blog/zero-js-by-default.skin +127 -0
  24. package/site/pages/carousel/index.wd +1 -0
  25. package/site/pages/compass/index.wd +1 -0
  26. package/site/pages/data.wd +3 -0
  27. package/site/pages/docs/-relative-note.wd +4 -0
  28. package/site/pages/docs/index.skin +27 -0
  29. package/site/pages/docs/index.wd +70 -13
  30. package/site/pages/folio/-style.wd +4 -0
  31. package/site/pages/folio/cart.wd +1 -0
  32. package/site/pages/folio/index.wd +1 -0
  33. package/site/pages/forge/index.wd +1 -0
  34. package/site/pages/highlight/index.wd +1 -0
  35. package/site/pages/index.wd +1 -0
  36. package/site/pages/interactions/index.wd +1 -0
  37. package/site/pages/ledger/index.wd +1 -0
  38. package/site/pages/loops.wd +1 -0
  39. package/site/pages/media/index.wd +1 -0
  40. package/site/pages/pulse/index.wd +1 -0
  41. package/site/pages/reactive.wd +1 -0
  42. package/site/pages/scoped/index.wd +1 -0
  43. package/site/pages/showcase/index.wd +1 -0
  44. package/site/pages/store.wd +1 -0
  45. package/site/pages/theme.wd +1 -0
  46. package/src/builder.js +444 -65
  47. package/src/cli.js +96 -18
  48. package/src/compiler/actions.js +303 -0
  49. package/src/compiler/body.js +36 -24
  50. package/src/compiler/context.js +56 -6
  51. package/src/compiler/directives.js +31 -1470
  52. package/src/compiler/fetch.js +159 -0
  53. package/src/compiler/format.js +17 -4
  54. package/src/compiler/forms.js +456 -0
  55. package/src/compiler/frontmatter.js +10 -5
  56. package/src/compiler/includes.js +66 -1
  57. package/src/compiler/index.js +15 -3
  58. package/src/compiler/loops.js +53 -21
  59. package/src/compiler/markdown.js +65 -15
  60. package/src/compiler/media.js +133 -0
  61. package/src/compiler/page.js +59 -75
  62. package/src/compiler/state.js +168 -0
  63. package/src/compiler/structure.js +377 -0
  64. package/src/dev.js +76 -0
  65. package/src/headers.js +56 -17
  66. package/src/router.js +12 -1
  67. package/src/runtime.js +5 -3
  68. package/src/statics.js +63 -12
  69. package/src/templates/blog/README.md +13 -4
  70. package/src/templates/blog/site/_/nav.wd +4 -0
  71. package/src/templates/blog/site/pages/index.skin +3 -0
  72. package/src/templates/blog/site/pages/index.wd +10 -4
  73. package/src/templates/blog/site/pages/posts/_schema.wd +16 -0
  74. package/src/templates/blog/site/pages/posts/hello-darkmown.md +2 -0
  75. package/src/templates/blog/site/pages/posts/hello-darkmown.skin +59 -0
  76. package/src/templates/blog/site/pages/posts/markdown-native.md +9 -4
  77. package/src/templates/blog/site/pages/posts/markdown-native.skin +59 -0
  78. package/src/templates/dashboard/site/_/nav.wd +4 -0
  79. package/src/templates/dashboard/site/pages/index.wd +1 -0
  80. package/src/templates/landing/site/_/nav.wd +4 -0
  81. package/src/templates/landing/site/pages/index.wd +1 -0
  82. package/src/templates/starter/site/_/nav.wd +4 -0
  83. package/src/templates/starter/site/pages/about.md +18 -1
  84. package/src/templates/starter/site/pages/about.skin +69 -0
  85. package/src/templates/starter/site/pages/index.wd +1 -0
  86. package/src/templates/store/site/_/nav.wd +4 -0
  87. package/src/templates/store/site/pages/index.wd +1 -0
  88. package/src/templates/blog/site/_/posts.json +0 -16
@@ -0,0 +1,127 @@
1
+ /* Colocated post styling — a .skin next to a .md attaches automatically, so
2
+ these prose pages stay static (zero JS) yet still read on-brand. A .md can't
3
+ @include /base.wd, so this reading skin carries the site's tokens itself.
4
+ Kept in sync with site/_/base.skin; copy it beside each new post. */
5
+
6
+ tokens
7
+ ink #1c1814
8
+ ink-soft #4b4239
9
+ muted #756a5d
10
+ paper #faf8f3
11
+ panel #ffffff
12
+ sunk #efe9dc
13
+ line #e7e1d4
14
+ accent #0f6b5e
15
+ accent-deep #0a4f45
16
+ accent-soft #e3f0ec
17
+ code-bg #1b2420
18
+ code-ink #e9efe7
19
+ code-line #2c3b34
20
+ radius 12px
21
+
22
+ tokens dark
23
+ ink #ece6db
24
+ ink-soft #c3bbab
25
+ muted #9a9081
26
+ paper #14110d
27
+ panel #1e1913
28
+ sunk #221d17
29
+ line #2f281f
30
+ accent #5ccab4
31
+ accent-deep #8fe0cf
32
+ accent-soft #16302a
33
+ code-bg #100d0a
34
+ code-ink #e4ebe2
35
+ code-line #2a231b
36
+
37
+ html
38
+ bg $paper
39
+
40
+ page
41
+ max-width 720px
42
+ margin 0 auto
43
+ padding 3rem 1.5rem 5rem
44
+ font-family ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, Cambria, serif
45
+ font-size 18px
46
+ line-height 1.7
47
+ color $ink
48
+ bg $paper
49
+ -webkit-font-smoothing antialiased
50
+ text-rendering optimizeLegibility
51
+
52
+ h1
53
+ font-size clamp(2rem, 5vw, 2.8rem)
54
+ line-height 1.12
55
+ letter-spacing -.022em
56
+ margin 1rem 0 1.25rem
57
+ color $ink
58
+
59
+ h2
60
+ font-size 1.5rem
61
+ line-height 1.2
62
+ letter-spacing -.012em
63
+ margin 2.4rem 0 .6rem
64
+ color $ink
65
+
66
+ h3
67
+ font-size 1.2rem
68
+ margin 1.8rem 0 .4rem
69
+ color $ink
70
+
71
+ p
72
+ margin 1.1rem 0
73
+
74
+ a
75
+ color $accent
76
+ text-underline-offset 2px
77
+
78
+ a:hover
79
+ color $accent-deep
80
+
81
+ strong
82
+ font-weight 650
83
+ color $ink
84
+
85
+ hr
86
+ border 0
87
+ border-top 1px solid $line
88
+ margin 2.5rem 0
89
+
90
+ ul, ol
91
+ margin 1.1rem 0
92
+ padding-left 1.4rem
93
+
94
+ li
95
+ margin .35rem 0
96
+
97
+ code
98
+ font-family ui-monospace, "SF Mono", Menlo, Consolas, monospace
99
+ font-size .88em
100
+ bg $sunk
101
+ color $accent-deep
102
+ padding .12rem .36rem
103
+ radius 6px
104
+
105
+ pre
106
+ bg $code-bg
107
+ color $code-ink
108
+ padding 1.2rem 1.35rem
109
+ margin 1.5rem 0
110
+ radius $radius
111
+ overflow-x auto
112
+ line-height 1.62
113
+ font-size .87rem
114
+ box-shadow inset 0 0 0 1px $code-line
115
+
116
+ pre code
117
+ bg transparent
118
+ color inherit
119
+ padding 0
120
+ font-size 1em
121
+
122
+ blockquote
123
+ margin 1.6rem 0
124
+ padding .6rem 1.3rem
125
+ border-left 3px solid $accent
126
+ color $ink-soft
127
+ font-style italic
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Swiper — a draggable carousel wired to wd.subscribe
3
4
  description: An image carousel with arrow-key nav, pointer drag / swipe, and a sliding transition — the things a deliberately tiny framework leaves to you. Darkmown owns the reactive slide state, the dots, and the readout declaratively; a colocated index.js adds the gestures through window.wd.subscribe and window.wd.set.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Compass — find your match
3
4
  description: A guided product-finder quiz built in one Markdown file. Four questions advance a state machine with :if branching, accumulate scores with :button actions, and a :computed recommendation lands the perfect pick. Zero hand-written JavaScript.
4
5
  image: https://darkmown.com/__wd/media/og-compass.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Data, forms, and persistence
3
4
  transitions: true
4
5
  ---
@@ -38,6 +39,8 @@ That loop also proves `:if` over loop items: the lead badge is decided per row.
38
39
 
39
40
  Every `:fetch` tracks the whole lifecycle — `loading`, `error`, `empty`, and data — so a page can answer each one. Here the same primitive shows an **error** (a missing URL 404s) and an **empty** result (a file that is just `[]`):
40
41
 
42
+ *(Heads up: the browser console will log a `404` for `intentionally-missing.json` on this page — that failure is deliberate, so the error branch below has something real to show.)*
43
+
41
44
  :fetch broken from "/__wd/data/intentionally-missing.json"
42
45
 
43
46
  :if broken_error
@@ -1,3 +1,7 @@
1
+ ---
2
+ html: true
3
+ ---
4
+
1
5
  <section class="feature-card">
2
6
 
3
7
  **Relative include:** this hidden block lives beside the docs page as `-relative-note.wd`, so it can be included without becoming a route.
@@ -17,6 +17,33 @@ main.docs
17
17
  color $ink-soft
18
18
  max-width 60ch
19
19
 
20
+ /* On-page TOC — a compact multi-column block of plain # anchors. */
21
+ .docs-toc
22
+ border 1px solid $line
23
+ bg $panel
24
+ radius $radius
25
+ padding 1.1rem 1.35rem
26
+ margin 1.25rem 0 .5rem
27
+
28
+ .docs-toc ul
29
+ columns 2
30
+ column-gap 2.5rem
31
+ margin .55rem 0 0
32
+ padding 0
33
+ list-style none
34
+
35
+ .docs-toc li
36
+ margin .18rem 0
37
+ break-inside avoid
38
+ font-size .95rem
39
+
40
+ .docs-toc a
41
+ font-weight 500
42
+
43
+ @media (max-width: 600px)
44
+ .docs-toc ul
45
+ columns 1
46
+
20
47
  /* Section headings get a quiet rule above them so the long page scans. */
21
48
  main.docs h2
22
49
  margin-top 3rem
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Darkmown docs
3
4
  description: Learn Darkmown routing, directives, state, loops, forms, and the static-to-reactive model.
4
5
  transitions: true
@@ -17,6 +18,40 @@ transitions: true
17
18
  Darkmown is a Markdown-native framework for mostly-static sites with tiny reactive islands. Pages are plain files: `.md` stays pure CommonMark, and `.wd` ("whateverdown") is the same Markdown plus first-party directives.
18
19
  </header>
19
20
 
21
+ <nav class="docs-toc" aria-label="On this page">
22
+
23
+ **On this page**
24
+
25
+ - [Install](#install)
26
+ - [The two formats](#the-two-formats)
27
+ - [Routing rules](#routing-rules)
28
+ - [Includes](#includes)
29
+ - [Loops](#loops)
30
+ - [Content collections](#content-collections)
31
+ - [Interpolation](#interpolation)
32
+ - [Frontmatter](#frontmatter)
33
+ - [SEO & feeds](#seo--feeds)
34
+ - [Drafts](#drafts)
35
+ - [Instant navigation](#instant-navigation)
36
+ - [Images](#images)
37
+ - [Sections](#sections)
38
+ - [Reactive directives](#reactive-directives)
39
+ - [Fetching data](#fetching-data)
40
+ - [Global state — `:store`](#global-state--store)
41
+ - [Forms and persistence](#forms-and-persistence)
42
+ - [Backends & deploy](#backends--deploy)
43
+ - [Interactions](#interactions--slider-sortable-carousel)
44
+ - [Theme toggle — `:theme`](#theme-toggle--theme)
45
+ - [Scoped styles — `scoped`](#scoped-styles--scoped)
46
+ - [Media directives](#media--video-audio-embed)
47
+ - [Syntax highlighting](#syntax-highlighting)
48
+ - [Trust boundary](#trust-boundary)
49
+ - [Colocation](#colocation)
50
+ - [Spec status](#spec-status)
51
+ </nav>
52
+
53
+ Every heading on every page gets a stable, GitHub-style `id` at build time (lowercased, punctuation stripped, duplicates deduped), so the links above — and deep links into any Darkmown page — are plain `#` anchors with zero JavaScript.
54
+
20
55
  @include ./-relative-note.wd
21
56
 
22
57
  ## Install
@@ -30,6 +65,10 @@ npm run dev
30
65
 
31
66
  Or add Darkmown to an existing project with `npm install -D @zvndev/darkmown`. The package is scoped; the command it installs is plain `darkmown`.
32
67
 
68
+ For editing, a [VS Code extension](https://github.com/ZVN-DEV/darkmown/tree/master/editors/vscode) gives `.wd` and `.skin` files syntax highlighting, snippets, and folding — install it from a `.vsix` built out of the framework repo (see its `editors/vscode` README; a Marketplace listing is pending).
69
+
70
+ `darkmown dev` rebuilds incrementally: it tracks which routes depend on each file (includes, colocated assets, loop data, collections), so editing one page or include recompiles only the routes it affects — and falls back to a full rebuild whenever a file is added, removed, or renamed. `routes.json`, the sitemap, and the RSS feed are regenerated on every rebuild, so they always describe the whole site.
71
+
33
72
  ## The two formats
34
73
 
35
74
  - `.md` is strict CommonMark. Directives, includes, and `{ bindings }` stay plain text, and the build prints a hint if it spots `.wd` syntax in a `.md` file.
@@ -54,7 +93,7 @@ Or add Darkmown to an existing project with `npm install -D @zvndev/darkmown`. T
54
93
 
55
94
  `@loop <things> into <thing>` is the only loop. The body is normal Markdown (or more directives) and repeats once per row:
56
95
 
57
- ```wd
96
+ ```wd-fragment
58
97
  @loop /features.json into card
59
98
  @include /feature-card.wd
60
99
  @endloop
@@ -72,7 +111,7 @@ Loops nest, and `{ card.title }` style dotted paths reach into each row.
72
111
 
73
112
  Add `where <predicate>` to filter a loop. Conditions join with `and` / `or`:
74
113
 
75
- ```wd
114
+ ```wd-fragment
76
115
  @loop /products.json into p where p.featured == true and p.price < 80
77
116
  - { p.name }
78
117
  @endloop
@@ -100,12 +139,22 @@ The predicate decides reactivity exactly like the source does: read only the row
100
139
  A `:button` inside a reactive loop can act on its own row — the basis for carts and to-do lists:
101
140
 
102
141
  ```wd
142
+ :state products = [{"id": 1, "name": "Aurora", "price": 49}]
143
+ :state cart = []
144
+
103
145
  @loop products into product
146
+ ::: card
147
+ **{ product.name }** — ${ product.price }
104
148
  :button "Add to cart" -> cart += product
105
149
  :::
150
+ @endloop
151
+
106
152
  @loop cart into line
153
+ ::: card
154
+ { line.name }
107
155
  :button "Remove" -> cart remove line
108
156
  :::
157
+ @endloop
109
158
  ```
110
159
 
111
160
  `cart += product` appends a copy of the current row to another `:state` list; `cart remove line` removes the current row from the list being looped. Both are checked against the enclosing loop at compile time — no JavaScript.
@@ -114,7 +163,7 @@ A `:button` inside a reactive loop can act on its own row — the basis for cart
114
163
 
115
164
  Shape a loop with clauses in a fixed order after `into`: `where`, then `sort by`, `reverse`, `offset`, `limit`.
116
165
 
117
- ```wd
166
+ ```wd-fragment
118
167
  @loop /posts.json into post sort by post.date desc limit 5
119
168
  { $number }. { post.title }
120
169
  @endloop
@@ -125,7 +174,7 @@ Shape a loop with clauses in a fixed order after `into`: `where`, then `sort by`
125
174
 
126
175
  Every row exposes meta variables — `{ $index }` (0-based), `{ $number }` (1-based), `{ $first }`, `{ $last }`, `{ $count }` — usable in interpolation and `:if`:
127
176
 
128
- ```wd
177
+ ```wd-fragment
129
178
  @loop products into product
130
179
  :if $first
131
180
  **Top pick:**
@@ -138,7 +187,7 @@ Every row exposes meta variables — `{ $index }` (0-based), `{ $number }` (1-ba
138
187
 
139
188
  Add an `@empty` branch to render a fallback when the loop produces no rows (after filtering and slicing):
140
189
 
141
- ```wd
190
+ ```wd-fragment
142
191
  @loop todos into todo
143
192
  - { todo.title }
144
193
  @empty
@@ -146,13 +195,15 @@ Nothing left to do.
146
195
  @endloop
147
196
  ```
148
197
 
198
+ A **missing** in-scope source is an empty list, not an error — `@loop meta.tags into tag` on a page whose frontmatter omits `tags` (an optional field) loops zero rows and renders the `@empty` branch. A value that is present but not a list is still a compile error with the file and line.
199
+
149
200
  These clauses stay build-time when the source and every argument are static — a sorted, limited loop over a JSON file ships zero JS. The loop turns reactive only when the source or a clause reads `:state`/`:store`/`:fetch` data.
150
201
 
151
202
  ## Content collections
152
203
 
153
204
  Any folder under `site/pages/` is a **collection** — loop it by its bare name. No `content/` root, no marker file: a `site/pages/blog/` folder of posts *is* the `blog` collection. The whole [blog on this site](/blog/) is one such loop.
154
205
 
155
- ```wd
206
+ ```wd-fragment
156
207
  @loop blog into post sort by post.date desc
157
208
  - [{ post.title }]({ post.url }) — { post.date }
158
209
  @endloop
@@ -179,7 +230,7 @@ Closed types: `string`, `number`, `boolean`, `date`, `string[]`, each with `?` f
179
230
 
180
231
  Add `paginate N` (collections only) to split a listing into static pages. Page 1 keeps the listing route; pages 2+ are at `/<route>/page/2/`, `/page/3/`, … all static HTML.
181
232
 
182
- ```wd
233
+ ```wd-fragment
183
234
  @loop blog into post sort by post.date desc paginate 5
184
235
  - [{ post.title }]({ post.url })
185
236
  @endloop
@@ -208,6 +259,8 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
208
259
 
209
260
  Shape a value for display with a pipe: `{ price | money }`, `{ joinedAt | date:"medium" }`, `{ ratio | percent }`. Pipes chain (`{ name | trim | capitalize }`) and take literal arguments (`{ total | money:"EUR" }`, `{ bio | truncate:80 }`). The same syntax works on static values (folded at build time, zero-JS) and live bindings (re-applied on render).
210
261
 
262
+ The value being piped must be **in scope** — a loop variable, `:state`/`:store`, an include argument, or `meta`. A bare literal like `{ 89 | money }` is not a name in scope, so it stays literal text (braces and all), exactly like any unresolved `{ … }`. A date-only value (`"2026-06-22"`) formats in UTC, so `date` prints the written calendar date on any build machine.
263
+
211
264
  The whitelist: `money`, `number`, `percent`, `round`, `date` / `time` / `datetime` (Intl-backed; `short` / `medium` / `long`), `upper` / `lower` / `capitalize`, `truncate:n` / `trim`, `pluralize:"item"`, and `default:"—"`. The five aggregates double as list pipes — `{ cart | count }`, `{ cart | sum:"price" | money }`, plus `avg` / `min` / `max`. No custom functions, nothing `eval`'d; an unknown pipe is a compile error. There is deliberately no relative "time ago" formatter, so builds stay reproducible.
212
265
 
213
266
  ## Frontmatter
@@ -223,6 +276,8 @@ tags: [sales, revenue, "q1, q2"]
223
276
 
224
277
  `{ meta.title }` prints a scalar and `{ meta.tags }` joins an array with `, `. You can loop a frontmatter array at build time — `@loop meta.tags into tag` — and the page stays static. Arrays are inline flow only (`[a, b]`); quoted items keep their commas.
225
278
 
279
+ `lang:` sets the document language on `<html lang>` per page (`lang: fr`, `lang: pt-BR`, …); it defaults to `en`. Every page also ships a visually-hidden-until-focused **skip-to-content link** targeting the main landmark — your own `<main>` is reused (an existing `id` wins as the target; an id-less one gets `id="main"`), and a page without one is wrapped in `<main id="main">`. Restyle it via `.wd-skip-link` in a skin. All compile-time — static pages stay zero-JS.
280
+
226
281
  ## SEO & feeds
227
282
 
228
283
  `darkmown build` writes `sitemap.xml`, `rss.xml`, and `robots.txt` — all build-time, zero client JS. There's no config file: the **home page** frontmatter carries the site's identity. Add `site_url` (absolute origin, no trailing slash) and the sitemap and feed switch on, prefixed with that origin and titled with the home `title`/`description`.
@@ -311,7 +366,7 @@ The aggregate's field is a bare row key (`price`, not `item.price`).
311
366
 
312
367
  A condition reads the same predicate grammar as `.class when` — a bare path (truthy), or the comparisons `==` `!=` `<` `<=` `>` `>=` `contains`, joined with `and`, `or`, and `not`. (`@loop … where` is the comparison-only subset.) Chain with `:else if` (any number; a bare `:else` must come last):
313
368
 
314
- ```wd
369
+ ```wd-fragment
315
370
  :if plan == "pro" or seats >= 5
316
371
  Pro plan
317
372
  :else if trialDays > 0 and not expired
@@ -327,7 +382,7 @@ A whole chain compiles to nested conditional regions, so it folds at build time
327
382
 
328
383
  A container class can react to state or the loop item. Static `.class` tokens stay as-is; add `when <predicate>` for a reactive one:
329
384
 
330
- ```wd
385
+ ```wd-fragment
331
386
  @loop products into p
332
387
  ::: card .product .on-sale when p.price < 50
333
388
  **{ p.name }** — ${ p.price }
@@ -362,7 +417,7 @@ A `:button "Label" -> action` mutates one `:state` or `:store` value. The vocabu
362
417
 
363
418
  Values are literals — a `"string"`, number, `true`/`false`/`null`, or inline JSON. Targets may be dotted paths (`cart.count++`), and one button can run several actions separated by `;` (applied in order, rendered once):
364
419
 
365
- ```wd
420
+ ```wd-fragment
366
421
  :button "Add to cart" -> cart.count++ ; cart.total += 9
367
422
  ```
368
423
 
@@ -388,6 +443,8 @@ No team members yet.
388
443
  :endif
389
444
  ```
390
445
 
446
+ The lifecycle regions announce themselves to assistive tech: a bare `:if name_loading` compiles with `role="status" aria-live="polite"` and `:if name_error` with `role="alert"` — compile-time attributes, no extra runtime JS. Write your own `role`/`aria-live` inside a region and nothing is added.
447
+
391
448
  Options after the URL: `method=` (default `GET`), `when=load|visible`, `timeout=<ms>`, `retry=<N>`, `headers=<key>`, and `body=<key>` (the last two name a `:state`/`:store` key). A URL can interpolate state (`"/api/users/{ userId }"`) and re-fetches automatically when that state changes; trigger a reload by hand with `:button "Reload" -> team refetch`. Loop a sub-path of fetched data with a dotted source — `@loop team.members into member`. URLs must be relative, `http(s)://`, or a leading `{ state }` interpolation — other schemes are rejected at compile time.
392
449
 
393
450
  ### Authenticated requests
@@ -530,10 +587,10 @@ The framework ships a small default `$code-*` set, so highlighting looks right o
530
587
  Darkmown is a trusted-author site generator: you compile content you wrote, the way you trust your own source. Three things define that boundary.
531
588
 
532
589
  - **Compile only content you authored.** Don't run `.md`/`.wd` you didn't write — user-generated content, third-party docs — through the compiler without sanitizing it first.
533
- - **Raw HTML passes through by default.** Markdown renders with `html: true`, so raw HTML is emitted verbatim and there is no built-in sanitizer. Set `html: false` in a page's frontmatter to drop raw HTML for untrusted content.
590
+ - **Raw HTML is escaped by default.** Markdown renders with `html: false`, so a stray `<script>` in content becomes inert text — safe for multi-author collections out of the box. Set `html: true` in a page's frontmatter to pass your own raw HTML through verbatim; there is no built-in sanitizer, so keep it off pages with content you didn't write.
534
591
  - **`:fetch` and `:form action=` have no host allowlist, and reactive pages need `unsafe-eval`.** Fetch/form URLs come straight from the page source — the compiler rejects non-http(s) schemes but does not restrict which hosts you call, so SSRF/exfiltration protection is the author's responsibility. The runtime compiles `:computed` / `@loop … where` / `.class when` expressions via `new Function`, so a reactive page needs `script-src 'unsafe-eval'` in your CSP. Static pages need neither.
535
592
 
536
- See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in the package for the full security model.
593
+ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/master/SECURITY.md) in the package for the full security model.
537
594
 
538
595
  ## Colocation
539
596
 
@@ -544,7 +601,7 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in
544
601
 
545
602
  ## Spec status
546
603
 
547
- The implementation is faithful to the original core thesis: Markdown-first authoring, no component ceremony, zero runtime on static pages, and tiny direct-DOM reactivity only when declared. `:store`, the full `:fetch` lifecycle (loading/error/empty states, dynamic URLs, `refetch`, dotted loop sources, authenticated `headers=` + `refresh=` token renewal), `:form` (including server round-trips), loop ergonomics (`sort by`/`limit`/`offset`/`reverse`/`@empty` and `$index`/`$number`/`$first`/`$last`/`$count`), `:if … :else if … :else`, reactive `.class when …`, `:effect`, `:computed`, and `persist` are all shipped and live — try them on the [Data & Forms page](/data/) and the [Reactive page](/reactive/). Still on the roadmap: a first-party server runtime (`site/api/`) and HTML-fragment swaps. See `docs/spec-alignment.md` in the package for the full audit.
604
+ The implementation is faithful to the original core thesis: Markdown-first authoring, no component ceremony, zero runtime on static pages, and tiny direct-DOM reactivity only when declared. `:store`, the full `:fetch` lifecycle (loading/error/empty states, dynamic URLs, `refetch`, dotted loop sources, authenticated `headers=` + `refresh=` token renewal), `:form` (including server round-trips), loop ergonomics (`sort by`/`limit`/`offset`/`reverse`/`@empty` and `$index`/`$number`/`$first`/`$last`/`$count`), `:if … :else if … :else`, reactive `.class when …`, `:effect`, `:computed`, and `persist` are all shipped and live — try them on the [Data & Forms page](/data/) and the [Reactive page](/reactive/). Still on the roadmap: HTML-fragment `swap` semantics and cart server sync. (The serverless `api/` model already shipped in 1.2.0 — see [Backends & deploy](#backends--deploy) above; a `.wd` backend DSL remains a deliberate non-goal.) See `docs/spec-alignment.md` in the package for the full audit.
548
605
 
549
606
  See these directives composed into real apps on the [showcase](/showcase/).
550
607
 
@@ -1,3 +1,7 @@
1
+ ---
2
+ html: true
3
+ ---
4
+
1
5
  <!-- Folio — shared style carrier.
2
6
 
3
7
  Both the catalog (/folio/) and the cart (/folio/cart/) `@include ./style.wd`
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Folio — your basket
3
4
  description: The Folio checkout. The same :store cart shared with the catalog, a per-row remove, and a form that round-trips to a server — all written in Markdown.
4
5
  image: https://darkmown.com/__wd/media/og-folio.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Folio — a boutique storefront
3
4
  description: A multi-page store written in pure Markdown. The cart persists across pages, reloads, and browser tabs; navigation is instant. No framework JavaScript written by hand.
4
5
  image: https://darkmown.com/__wd/media/og-folio.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Forge your plan — a live pricing configurator in Markdown
3
4
  description: Build your plan, toggle features, adjust the seat count, and watch the price, savings, and upsell hints update instantly. A classic "this needs React" interaction — written as one Markdown file with a reactive :computed pricing engine.
4
5
  image: https://darkmown.com/__wd/media/og-forge.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Syntax highlighting — code that dark-modes for free
3
4
  description: Build-time syntax highlighting in plain Markdown. Fenced code blocks highlight at compile time (zero client JS) and recolor with your theme through the same tokens dark / :theme system — flip the toggle and watch every language recolor at once.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Darkmown — Markdown that runs, not Markdown that needs React
3
4
  description: A live store widget — search filter, add-to-cart, running total — written as one Markdown file that runs. The same outcome in Markdoc needs a React component, hydration, and a build step. Compare them side by side.
4
5
  image: https://darkmown.com/__wd/media/og.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Interactions — sliders, drag & carousels in Markdown
3
4
  description: :slider, :sortable, and :carousel — rich interactions that compile to tiny pay-for-what-you-use modules, declared in plain Markdown. Sliders ship zero JS; sortable and carousel each load one small module, only on the pages that use them.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Ledger — a sortable expense table in Markdown
3
4
  description: A real data table written as one Markdown file — clickable-header sorting, a live filter, running totals, and a light/dark toggle. Format pipes (| money, | date), aggregates (sum, avg, max), reactive sort, and :theme — all declarative, no hand-written JavaScript.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Loop ergonomics
3
4
  transitions: true
4
5
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Media — video, audio, and embeds in Markdown
3
4
  description: Three one-line directives — :video, :audio, :embed — compile to hardened, accessible, privacy-friendly players. No framework JavaScript ships: a media page stays zero-JS, just like static Markdown.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Pulse — service status
3
4
  description: A calm, dark observability panel built in one Markdown file — a live :fetch board, build-time rollups, and a keyed services table, all with zero hand-written JavaScript.
4
5
  image: https://darkmown.com/__wd/media/og-pulse.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Reactive Markdown
3
4
  transitions: true
4
5
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Scoped styles — same class name, zero collision
3
4
  description: Two components reuse the class name .card and never clash. Compile-time scoped styles stamp each component's subtree with a path-derived id and rewrite its skin's selectors to match only there. Zero runtime JS — a static page stays static.
4
5
  transitions: true
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Showcase — real apps built in Markdown
3
4
  description: Five complete, beautiful apps — a storefront, a live dashboard, a pricing configurator, a product-finder quiz, and a sortable expense ledger — each one a readable Markdown file. See the app, then view its source.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Global store
3
4
  transitions: true
4
5
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Dark-mode tokens
3
4
  transitions: true
4
5
  ---