@zvndev/darkmown 1.5.0 → 2.0.1

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 (95) hide show
  1. package/AGENTS.md +18 -4
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +49 -19
  4. package/SECURITY.md +21 -18
  5. package/docs/cli.md +5 -1
  6. package/docs/spec-alignment.md +21 -7
  7. package/package.json +3 -2
  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 +5 -1
  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 +2 -1
  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.md +1 -1
  24. package/site/pages/blog/zero-js-by-default.skin +127 -0
  25. package/site/pages/carousel/index.wd +1 -0
  26. package/site/pages/compass/index.wd +1 -0
  27. package/site/pages/data.wd +3 -0
  28. package/site/pages/docs/-relative-note.wd +4 -0
  29. package/site/pages/docs/index.skin +27 -0
  30. package/site/pages/docs/index.wd +85 -14
  31. package/site/pages/folio/-style.wd +4 -0
  32. package/site/pages/folio/cart.wd +1 -0
  33. package/site/pages/folio/index.wd +1 -0
  34. package/site/pages/forge/index.wd +1 -0
  35. package/site/pages/highlight/index.wd +1 -0
  36. package/site/pages/index.js +19 -0
  37. package/site/pages/index.skin +46 -1
  38. package/site/pages/index.wd +6 -3
  39. package/site/pages/interactions/index.wd +1 -0
  40. package/site/pages/ledger/index.wd +1 -0
  41. package/site/pages/loops.wd +1 -0
  42. package/site/pages/media/index.wd +1 -0
  43. package/site/pages/pulse/index.wd +1 -0
  44. package/site/pages/reactive.wd +1 -0
  45. package/site/pages/scoped/index.wd +1 -0
  46. package/site/pages/showcase/index.wd +2 -1
  47. package/site/pages/store.wd +1 -0
  48. package/site/pages/theme.wd +1 -0
  49. package/src/builder.js +444 -65
  50. package/src/cli.js +96 -18
  51. package/src/compiler/actions.js +303 -0
  52. package/src/compiler/body.js +36 -24
  53. package/src/compiler/context.js +65 -6
  54. package/src/compiler/directives.js +31 -1470
  55. package/src/compiler/fetch.js +159 -0
  56. package/src/compiler/format.js +17 -4
  57. package/src/compiler/forms.js +456 -0
  58. package/src/compiler/frontmatter.js +10 -5
  59. package/src/compiler/includes.js +75 -4
  60. package/src/compiler/index.js +15 -3
  61. package/src/compiler/loops.js +85 -23
  62. package/src/compiler/markdown.js +65 -15
  63. package/src/compiler/media.js +133 -0
  64. package/src/compiler/page.js +88 -77
  65. package/src/compiler/state.js +168 -0
  66. package/src/compiler/structure.js +399 -0
  67. package/src/dev.js +76 -0
  68. package/src/headers.js +56 -17
  69. package/src/router.js +12 -1
  70. package/src/runtime.js +5 -3
  71. package/src/statics.js +63 -12
  72. package/src/templates/blog/README.md +13 -4
  73. package/src/templates/blog/site/_/nav.wd +7 -6
  74. package/src/templates/blog/site/pages/index.skin +10 -4
  75. package/src/templates/blog/site/pages/index.wd +11 -6
  76. package/src/templates/blog/site/pages/posts/_schema.wd +16 -0
  77. package/src/templates/blog/site/pages/posts/hello-darkmown.md +2 -0
  78. package/src/templates/blog/site/pages/posts/hello-darkmown.skin +59 -0
  79. package/src/templates/blog/site/pages/posts/markdown-native.md +9 -4
  80. package/src/templates/blog/site/pages/posts/markdown-native.skin +59 -0
  81. package/src/templates/dashboard/site/_/nav.wd +7 -6
  82. package/src/templates/dashboard/site/pages/index.skin +7 -4
  83. package/src/templates/dashboard/site/pages/index.wd +2 -2
  84. package/src/templates/landing/site/_/nav.wd +7 -6
  85. package/src/templates/landing/site/pages/index.skin +13 -4
  86. package/src/templates/landing/site/pages/index.wd +22 -8
  87. package/src/templates/starter/site/_/nav.wd +8 -7
  88. package/src/templates/starter/site/pages/about.md +8 -2
  89. package/src/templates/starter/site/pages/about.skin +44 -0
  90. package/src/templates/starter/site/pages/index.skin +10 -6
  91. package/src/templates/starter/site/pages/index.wd +2 -2
  92. package/src/templates/store/site/_/nav.wd +7 -6
  93. package/src/templates/store/site/pages/index.skin +7 -4
  94. package/src/templates/store/site/pages/index.wd +2 -2
  95. 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
@@ -15,7 +15,7 @@ none of them pay for it.
15
15
 
16
16
  That is the whole bargain. A `.md` file is plain CommonMark. Rename it to `.wd`
17
17
  and you unlock directives — state, loops, conditionals — but you only ship the
18
- ~7.4 KB runtime on the pages that actually use them. A listing built from a
18
+ ~7.5 KB runtime on the pages that actually use them. A listing built from a
19
19
  collection loop resolves entirely at build time, so it stays static HTML.
20
20
 
21
21
  ## The extension is the feature gate
@@ -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,41 @@ 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
+ - [CLI reference](#cli-reference)
51
+ - [Spec status](#spec-status)
52
+ </nav>
53
+
54
+ 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.
55
+
20
56
  @include ./-relative-note.wd
21
57
 
22
58
  ## Install
@@ -30,6 +66,10 @@ npm run dev
30
66
 
31
67
  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
68
 
69
+ 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).
70
+
71
+ `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.
72
+
33
73
  ## The two formats
34
74
 
35
75
  - `.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 +94,7 @@ Or add Darkmown to an existing project with `npm install -D @zvndev/darkmown`. T
54
94
 
55
95
  `@loop <things> into <thing>` is the only loop. The body is normal Markdown (or more directives) and repeats once per row:
56
96
 
57
- ```wd
97
+ ```wd-fragment
58
98
  @loop /features.json into card
59
99
  @include /feature-card.wd
60
100
  @endloop
@@ -72,7 +112,7 @@ Loops nest, and `{ card.title }` style dotted paths reach into each row.
72
112
 
73
113
  Add `where <predicate>` to filter a loop. Conditions join with `and` / `or`:
74
114
 
75
- ```wd
115
+ ```wd-fragment
76
116
  @loop /products.json into p where p.featured == true and p.price < 80
77
117
  - { p.name }
78
118
  @endloop
@@ -100,12 +140,22 @@ The predicate decides reactivity exactly like the source does: read only the row
100
140
  A `:button` inside a reactive loop can act on its own row — the basis for carts and to-do lists:
101
141
 
102
142
  ```wd
143
+ :state products = [{"id": 1, "name": "Aurora", "price": 49}]
144
+ :state cart = []
145
+
103
146
  @loop products into product
147
+ ::: card
148
+ **{ product.name }** — ${ product.price }
104
149
  :button "Add to cart" -> cart += product
105
150
  :::
151
+ @endloop
152
+
106
153
  @loop cart into line
154
+ ::: card
155
+ { line.name }
107
156
  :button "Remove" -> cart remove line
108
157
  :::
158
+ @endloop
109
159
  ```
110
160
 
111
161
  `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 +164,7 @@ A `:button` inside a reactive loop can act on its own row — the basis for cart
114
164
 
115
165
  Shape a loop with clauses in a fixed order after `into`: `where`, then `sort by`, `reverse`, `offset`, `limit`.
116
166
 
117
- ```wd
167
+ ```wd-fragment
118
168
  @loop /posts.json into post sort by post.date desc limit 5
119
169
  { $number }. { post.title }
120
170
  @endloop
@@ -125,7 +175,7 @@ Shape a loop with clauses in a fixed order after `into`: `where`, then `sort by`
125
175
 
126
176
  Every row exposes meta variables — `{ $index }` (0-based), `{ $number }` (1-based), `{ $first }`, `{ $last }`, `{ $count }` — usable in interpolation and `:if`:
127
177
 
128
- ```wd
178
+ ```wd-fragment
129
179
  @loop products into product
130
180
  :if $first
131
181
  **Top pick:**
@@ -138,7 +188,7 @@ Every row exposes meta variables — `{ $index }` (0-based), `{ $number }` (1-ba
138
188
 
139
189
  Add an `@empty` branch to render a fallback when the loop produces no rows (after filtering and slicing):
140
190
 
141
- ```wd
191
+ ```wd-fragment
142
192
  @loop todos into todo
143
193
  - { todo.title }
144
194
  @empty
@@ -146,13 +196,15 @@ Nothing left to do.
146
196
  @endloop
147
197
  ```
148
198
 
199
+ 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.
200
+
149
201
  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
202
 
151
203
  ## Content collections
152
204
 
153
205
  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
206
 
155
- ```wd
207
+ ```wd-fragment
156
208
  @loop blog into post sort by post.date desc
157
209
  - [{ post.title }]({ post.url }) — { post.date }
158
210
  @endloop
@@ -179,7 +231,7 @@ Closed types: `string`, `number`, `boolean`, `date`, `string[]`, each with `?` f
179
231
 
180
232
  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
233
 
182
- ```wd
234
+ ```wd-fragment
183
235
  @loop blog into post sort by post.date desc paginate 5
184
236
  - [{ post.title }]({ post.url })
185
237
  @endloop
@@ -208,6 +260,8 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
208
260
 
209
261
  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
262
 
263
+ 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.
264
+
211
265
  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
266
 
213
267
  ## Frontmatter
@@ -223,6 +277,8 @@ tags: [sales, revenue, "q1, q2"]
223
277
 
224
278
  `{ 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
279
 
280
+ `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.
281
+
226
282
  ## SEO & feeds
227
283
 
228
284
  `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`.
@@ -253,9 +309,11 @@ You don't size images by hand. Every `<img>` the compiler emits gets its intrins
253
309
 
254
310
  `::: section #id .class` opens a container and `:::` closes it. Sections scope state: a `:state` declared inside a section belongs to that section, so two sections can both declare `count` without colliding. Bindings and buttons resolve to the nearest scope.
255
311
 
312
+ A container named `nav` or `main` emits the real landmark element (`<nav class="nav">`, `<main class="main">`) so pages keep proper landmarks without raw HTML. Any other name stays a `<div>` with that class.
313
+
256
314
  ## Reactive directives
257
315
 
258
- Reactive pages opt into `/__wd/runtime.js` (~7.4 KB gzipped, under a CI-enforced 8 KB budget). Static pages do not.
316
+ Reactive pages opt into `/__wd/runtime.js` (~7.5 KB gzipped, under a CI-enforced 8 KB budget). Static pages do not.
259
317
 
260
318
  ```wd
261
319
  :state count = 0
@@ -311,7 +369,7 @@ The aggregate's field is a bare row key (`price`, not `item.price`).
311
369
 
312
370
  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
371
 
314
- ```wd
372
+ ```wd-fragment
315
373
  :if plan == "pro" or seats >= 5
316
374
  Pro plan
317
375
  :else if trialDays > 0 and not expired
@@ -327,7 +385,7 @@ A whole chain compiles to nested conditional regions, so it folds at build time
327
385
 
328
386
  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
387
 
330
- ```wd
388
+ ```wd-fragment
331
389
  @loop products into p
332
390
  ::: card .product .on-sale when p.price < 50
333
391
  **{ p.name }** — ${ p.price }
@@ -362,7 +420,7 @@ A `:button "Label" -> action` mutates one `:state` or `:store` value. The vocabu
362
420
 
363
421
  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
422
 
365
- ```wd
423
+ ```wd-fragment
366
424
  :button "Add to cart" -> cart.count++ ; cart.total += 9
367
425
  ```
368
426
 
@@ -388,6 +446,8 @@ No team members yet.
388
446
  :endif
389
447
  ```
390
448
 
449
+ 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.
450
+
391
451
  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
452
 
393
453
  ### Authenticated requests
@@ -530,10 +590,10 @@ The framework ships a small default `$code-*` set, so highlighting looks right o
530
590
  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
591
 
532
592
  - **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.
593
+ - **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
594
  - **`: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
595
 
536
- See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in the package for the full security model.
596
+ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/master/SECURITY.md) in the package for the full security model.
537
597
 
538
598
  ## Colocation
539
599
 
@@ -542,9 +602,20 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in
542
602
  - Both work for included fragments too, by basename.
543
603
  - Any other file under `site/pages/` (an image, font, PDF — anything that isn't a `.md`/`.wd` route or a basename-matched `.skin`/`.js`) copies to `dist/` at its own path with the right content-type. So `site/pages/logo.svg` is served at `/logo.svg`. Assets below hidden path segments (`.`, `-`, or `_`) and symlinked page assets are skipped, matching the routing privacy rule. Use the `site/_/` shelf for assets shared across pages; colocate the ones a single page owns.
544
604
 
605
+ ## CLI reference
606
+
607
+ The `darkmown` command (installed by `npx @zvndev/darkmown`) is the whole tool — every command is one word:
608
+
609
+ - `darkmown init [dir] [--template <name>]` — scaffold a new site from a template (`starter`, `blog`, `store`, `dashboard`, `landing`). Existing files are never overwritten.
610
+ - `darkmown dev` — the live compiler: browser reload, an in-browser error overlay, incremental rebuilds, and a local runner for `api/*` functions.
611
+ - `darkmown build [--target cloudflare] [--drafts]` — write 100% static output to `dist/` (plus `sitemap.xml` / `rss.xml` / `robots.txt`); `--drafts` includes `draft: true` pages for a staging deploy.
612
+ - `darkmown serve` — preview the built `dist/` locally (static only; use `dev` to exercise `api/*`).
613
+ - `darkmown deploy <vercel|cloudflare> [--prod]` — build (target-aware) and deploy through the platform CLI, printing your URL; an unauthenticated CLI surfaces the login to run first.
614
+ - `darkmown version` / `darkmown help` — print the installed version, or the full command summary and directive syntax.
615
+
545
616
  ## Spec status
546
617
 
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.
618
+ 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
619
 
549
620
  See these directives composed into real apps on the [showcase](/showcase/).
550
621
 
@@ -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,3 +1,22 @@
1
+ const copyBtn = document.querySelector("[data-copy-cmd]");
2
+
3
+ if (copyBtn) {
4
+ copyBtn.addEventListener("click", async () => {
5
+ const cmd = copyBtn.previousElementSibling?.textContent ?? "";
6
+ try {
7
+ await navigator.clipboard.writeText(cmd);
8
+ copyBtn.textContent = "Copied";
9
+ copyBtn.classList.add("is-copied");
10
+ setTimeout(() => {
11
+ copyBtn.textContent = "Copy";
12
+ copyBtn.classList.remove("is-copied");
13
+ }, 1600);
14
+ } catch {
15
+ copyBtn.textContent = "Select + ⌘C";
16
+ }
17
+ });
18
+ }
19
+
1
20
  const button = document.querySelector("[data-wd-demo]");
2
21
 
3
22
  if (button) {