@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.
- package/AGENTS.md +17 -3
- package/CHANGELOG.md +28 -0
- package/README.md +44 -16
- package/SECURITY.md +21 -18
- package/docs/cli.md +5 -1
- package/docs/spec-alignment.md +19 -6
- package/package.json +1 -1
- package/site/_/base.skin +8 -0
- package/site/_/base.wd +4 -0
- package/site/_/feature-card.wd +4 -0
- package/site/_/footer.wd +4 -0
- package/site/_/nav.wd +4 -0
- package/site/_/scoped-promo.wd +4 -0
- package/site/_/scoped-stat.wd +4 -0
- package/site/pages/404.wd +1 -0
- package/site/pages/app.wd +1 -0
- package/site/pages/blog/collections-and-pagination.skin +127 -0
- package/site/pages/blog/drafts-that-never-leak.skin +127 -0
- package/site/pages/blog/index.wd +1 -0
- package/site/pages/blog/shipping-feeds.skin +127 -0
- package/site/pages/blog/typed-frontmatter.skin +127 -0
- package/site/pages/blog/unfinished-thoughts.skin +127 -0
- package/site/pages/blog/zero-js-by-default.skin +127 -0
- package/site/pages/carousel/index.wd +1 -0
- package/site/pages/compass/index.wd +1 -0
- package/site/pages/data.wd +3 -0
- package/site/pages/docs/-relative-note.wd +4 -0
- package/site/pages/docs/index.skin +27 -0
- package/site/pages/docs/index.wd +70 -13
- package/site/pages/folio/-style.wd +4 -0
- package/site/pages/folio/cart.wd +1 -0
- package/site/pages/folio/index.wd +1 -0
- package/site/pages/forge/index.wd +1 -0
- package/site/pages/highlight/index.wd +1 -0
- package/site/pages/index.wd +1 -0
- package/site/pages/interactions/index.wd +1 -0
- package/site/pages/ledger/index.wd +1 -0
- package/site/pages/loops.wd +1 -0
- package/site/pages/media/index.wd +1 -0
- package/site/pages/pulse/index.wd +1 -0
- package/site/pages/reactive.wd +1 -0
- package/site/pages/scoped/index.wd +1 -0
- package/site/pages/showcase/index.wd +1 -0
- package/site/pages/store.wd +1 -0
- package/site/pages/theme.wd +1 -0
- package/src/builder.js +444 -65
- package/src/cli.js +96 -18
- package/src/compiler/actions.js +303 -0
- package/src/compiler/body.js +36 -24
- package/src/compiler/context.js +56 -6
- package/src/compiler/directives.js +31 -1470
- package/src/compiler/fetch.js +159 -0
- package/src/compiler/format.js +17 -4
- package/src/compiler/forms.js +456 -0
- package/src/compiler/frontmatter.js +10 -5
- package/src/compiler/includes.js +66 -1
- package/src/compiler/index.js +15 -3
- package/src/compiler/loops.js +53 -21
- package/src/compiler/markdown.js +65 -15
- package/src/compiler/media.js +133 -0
- package/src/compiler/page.js +59 -75
- package/src/compiler/state.js +168 -0
- package/src/compiler/structure.js +377 -0
- package/src/dev.js +76 -0
- package/src/headers.js +56 -17
- package/src/router.js +12 -1
- package/src/runtime.js +5 -3
- package/src/statics.js +63 -12
- package/src/templates/blog/README.md +13 -4
- package/src/templates/blog/site/_/nav.wd +4 -0
- package/src/templates/blog/site/pages/index.skin +3 -0
- package/src/templates/blog/site/pages/index.wd +10 -4
- package/src/templates/blog/site/pages/posts/_schema.wd +16 -0
- package/src/templates/blog/site/pages/posts/hello-darkmown.md +2 -0
- package/src/templates/blog/site/pages/posts/hello-darkmown.skin +59 -0
- package/src/templates/blog/site/pages/posts/markdown-native.md +9 -4
- package/src/templates/blog/site/pages/posts/markdown-native.skin +59 -0
- package/src/templates/dashboard/site/_/nav.wd +4 -0
- package/src/templates/dashboard/site/pages/index.wd +1 -0
- package/src/templates/landing/site/_/nav.wd +4 -0
- package/src/templates/landing/site/pages/index.wd +1 -0
- package/src/templates/starter/site/_/nav.wd +4 -0
- package/src/templates/starter/site/pages/about.md +18 -1
- package/src/templates/starter/site/pages/about.skin +69 -0
- package/src/templates/starter/site/pages/index.wd +1 -0
- package/src/templates/store/site/_/nav.wd +4 -0
- package/src/templates/store/site/pages/index.wd +1 -0
- 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
|
package/site/pages/data.wd
CHANGED
|
@@ -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
|
|
@@ -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
|
package/site/pages/docs/index.wd
CHANGED
|
@@ -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
|
|
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/
|
|
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:
|
|
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
|
|
package/site/pages/folio/cart.wd
CHANGED
|
@@ -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
|
package/site/pages/index.wd
CHANGED
|
@@ -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
|
package/site/pages/loops.wd
CHANGED
|
@@ -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
|
package/site/pages/reactive.wd
CHANGED
|
@@ -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
|
package/site/pages/store.wd
CHANGED