@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.
- package/AGENTS.md +18 -4
- package/CHANGELOG.md +40 -0
- package/README.md +49 -19
- package/SECURITY.md +21 -18
- package/docs/cli.md +5 -1
- package/docs/spec-alignment.md +21 -7
- package/package.json +3 -2
- package/site/_/base.skin +8 -0
- package/site/_/base.wd +4 -0
- package/site/_/feature-card.wd +4 -0
- package/site/_/footer.wd +5 -1
- 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 +2 -1
- 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.md +1 -1
- 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 +85 -14
- 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.js +19 -0
- package/site/pages/index.skin +46 -1
- package/site/pages/index.wd +6 -3
- 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 +2 -1
- 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 +65 -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 +75 -4
- package/src/compiler/index.js +15 -3
- package/src/compiler/loops.js +85 -23
- package/src/compiler/markdown.js +65 -15
- package/src/compiler/media.js +133 -0
- package/src/compiler/page.js +88 -77
- package/src/compiler/state.js +168 -0
- package/src/compiler/structure.js +399 -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 +7 -6
- package/src/templates/blog/site/pages/index.skin +10 -4
- package/src/templates/blog/site/pages/index.wd +11 -6
- 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 +7 -6
- package/src/templates/dashboard/site/pages/index.skin +7 -4
- package/src/templates/dashboard/site/pages/index.wd +2 -2
- package/src/templates/landing/site/_/nav.wd +7 -6
- package/src/templates/landing/site/pages/index.skin +13 -4
- package/src/templates/landing/site/pages/index.wd +22 -8
- package/src/templates/starter/site/_/nav.wd +8 -7
- package/src/templates/starter/site/pages/about.md +8 -2
- package/src/templates/starter/site/pages/about.skin +44 -0
- package/src/templates/starter/site/pages/index.skin +10 -6
- package/src/templates/starter/site/pages/index.wd +2 -2
- package/src/templates/store/site/_/nav.wd +7 -6
- package/src/templates/store/site/pages/index.skin +7 -4
- package/src/templates/store/site/pages/index.wd +2 -2
- 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.
|
|
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
|
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,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.
|
|
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
|
|
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/
|
|
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:
|
|
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
|
|
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.js
CHANGED
|
@@ -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) {
|