@zvndev/darkmown 2.3.1-next.2.ga0699da → 2.3.1-next.4.g92bbead
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/package.json +1 -1
- package/site/_/base.skin +8 -0
- package/site/_/footer.wd +1 -1
- package/site/pages/index.js +26 -21
- package/site/pages/index.skin +111 -205
- package/site/pages/index.wd +115 -103
- package/site/pages/vs/markdoc.skin +53 -0
- package/site/pages/vs/markdoc.wd +96 -0
package/package.json
CHANGED
package/site/_/base.skin
CHANGED
|
@@ -484,6 +484,14 @@ code
|
|
|
484
484
|
color $accent-deep
|
|
485
485
|
padding .12rem .36rem
|
|
486
486
|
radius 6px
|
|
487
|
+
/* An inline token that wraps (`unsafe-eval` breaks at its hyphen) otherwise
|
|
488
|
+
leaves a half-open pill on each line: padding and rounding on the outer
|
|
489
|
+
edges only. `clone` gives every fragment the full box, so a wrapped token
|
|
490
|
+
reads as one styled run instead of a rendering artifact. Wrapping itself
|
|
491
|
+
is left alone on purpose: some inline tokens on this site run to 70+
|
|
492
|
+
characters, and `nowrap` would overflow the viewport on a phone. */
|
|
493
|
+
box-decoration-break clone
|
|
494
|
+
-webkit-box-decoration-break clone
|
|
487
495
|
|
|
488
496
|
h1 code, h2 code, h3 code
|
|
489
497
|
font-size .82em
|
package/site/_/footer.wd
CHANGED
|
@@ -5,7 +5,7 @@ html: true
|
|
|
5
5
|
<footer class="sitefooter">
|
|
6
6
|
<div class="foot-inner">
|
|
7
7
|
<div>
|
|
8
|
-
<strong>Darkmown</strong
|
|
8
|
+
<strong>Darkmown</strong>. Markdown that runs.
|
|
9
9
|
<div class="small">Zero JS on static pages · one ~7.7 KB runtime when reactive.</div>
|
|
10
10
|
</div>
|
|
11
11
|
<nav class="foot-links">
|
package/site/pages/index.js
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
// Colocated behavior for the landing page, discovered by basename
|
|
2
|
+
// (site/pages/index.wd -> site/pages/index.js). It is the only hand-written
|
|
3
|
+
// JavaScript on this page; everything else is directives.
|
|
4
|
+
//
|
|
5
|
+
// The page authors its install command as a plain ```sh fence so the source
|
|
6
|
+
// stays free of raw HTML. The copy button is added here instead: progressive
|
|
7
|
+
// enhancement, so the command is still readable and selectable with JS off.
|
|
2
8
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
const installBlock = document.querySelector(".hero pre > code");
|
|
10
|
+
|
|
11
|
+
if (installBlock && navigator.clipboard) {
|
|
12
|
+
const pre = installBlock.parentElement;
|
|
13
|
+
const button = document.createElement("button");
|
|
14
|
+
button.type = "button";
|
|
15
|
+
button.className = "copy-cmd";
|
|
16
|
+
button.textContent = "Copy";
|
|
17
|
+
button.setAttribute("aria-label", "Copy install command");
|
|
18
|
+
pre.classList.add("has-copy");
|
|
19
|
+
pre.append(button);
|
|
20
|
+
|
|
21
|
+
button.addEventListener("click", async () => {
|
|
6
22
|
try {
|
|
7
|
-
await navigator.clipboard.writeText(
|
|
8
|
-
|
|
9
|
-
|
|
23
|
+
await navigator.clipboard.writeText(installBlock.textContent.trim());
|
|
24
|
+
button.textContent = "Copied";
|
|
25
|
+
button.classList.add("is-copied");
|
|
10
26
|
setTimeout(() => {
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
button.textContent = "Copy";
|
|
28
|
+
button.classList.remove("is-copied");
|
|
13
29
|
}, 1600);
|
|
14
30
|
} catch {
|
|
15
|
-
|
|
31
|
+
button.textContent = "Select + ⌘C";
|
|
16
32
|
}
|
|
17
33
|
});
|
|
18
34
|
}
|
|
19
|
-
|
|
20
|
-
const button = document.querySelector("[data-wd-demo]");
|
|
21
|
-
|
|
22
|
-
if (button) {
|
|
23
|
-
button.addEventListener("click", () => {
|
|
24
|
-
button.classList.toggle("is-on");
|
|
25
|
-
button.textContent = button.classList.contains("is-on")
|
|
26
|
-
? "Colocated index.js is active"
|
|
27
|
-
: "Script is waiting";
|
|
28
|
-
});
|
|
29
|
-
}
|
package/site/pages/index.skin
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
/* Landing page
|
|
2
|
-
Only landing-specific layout lives here.
|
|
1
|
+
/* Landing page. Layers on /base.wd (tokens, type, components, dark mode).
|
|
2
|
+
Only landing-specific layout lives here.
|
|
3
|
+
|
|
4
|
+
The page itself contains no raw HTML: every block below is produced by a
|
|
5
|
+
`::: name` container, a markdown element, or a directive. Class names match
|
|
6
|
+
the container names in site/pages/index.wd. */
|
|
3
7
|
|
|
4
8
|
/* ── Hero ───────────────────────────────────────────────────────── */
|
|
5
9
|
|
|
@@ -9,173 +13,105 @@
|
|
|
9
13
|
margin-bottom 2.5rem
|
|
10
14
|
|
|
11
15
|
.hero h1
|
|
12
|
-
max-width
|
|
16
|
+
max-width 15ch
|
|
17
|
+
font-size clamp(2.4rem, 6vw, 3.6rem)
|
|
13
18
|
|
|
14
19
|
.hero h1 em
|
|
15
20
|
font-style italic
|
|
16
21
|
color $accent
|
|
17
22
|
|
|
18
23
|
.hero > p
|
|
19
|
-
max-width
|
|
24
|
+
max-width 60ch
|
|
20
25
|
font-size 1.18rem
|
|
21
26
|
color $ink-soft
|
|
22
27
|
margin 1rem 0 1.75rem
|
|
23
28
|
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
flex-wrap wrap
|
|
32
|
-
margin-bottom 1.5rem
|
|
33
|
-
|
|
34
|
-
.hero-install
|
|
35
|
-
display inline-flex
|
|
36
|
-
align-items center
|
|
37
|
-
gap .55rem
|
|
38
|
-
max-width 100%
|
|
39
|
-
margin 0 0 2rem
|
|
40
|
-
padding .7rem 1rem
|
|
29
|
+
/* The install command is a plain ```sh fence; index.js appends the copy
|
|
30
|
+
button, so the markup stays directive-only and works with JS off. */
|
|
31
|
+
.hero pre
|
|
32
|
+
position relative
|
|
33
|
+
max-width 34rem
|
|
34
|
+
margin 0 0 1.75rem
|
|
35
|
+
padding .85rem 1rem
|
|
41
36
|
border 1px solid $line
|
|
42
37
|
radius 10px
|
|
43
38
|
bg $sunk
|
|
44
|
-
font-family ui-monospace, SFMono-Regular, Menlo, monospace
|
|
45
|
-
overflow-x auto
|
|
46
39
|
|
|
47
|
-
.hero
|
|
48
|
-
content "$"
|
|
49
|
-
color $accent
|
|
50
|
-
font-weight 700
|
|
51
|
-
flex-shrink 0
|
|
52
|
-
|
|
53
|
-
.hero-install code
|
|
54
|
-
font-family inherit
|
|
40
|
+
.hero pre code
|
|
55
41
|
font-size .95rem
|
|
56
42
|
color $ink
|
|
57
43
|
white-space nowrap
|
|
58
44
|
|
|
59
|
-
.hero
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
.hero pre.has-copy
|
|
46
|
+
padding-right 5.5rem
|
|
47
|
+
|
|
48
|
+
.hero pre .copy-cmd
|
|
49
|
+
position absolute
|
|
50
|
+
top 50%
|
|
51
|
+
right .6rem
|
|
52
|
+
transform translateY(-50%)
|
|
53
|
+
padding .3rem .7rem
|
|
63
54
|
border 1px solid $line
|
|
64
55
|
radius 7px
|
|
65
|
-
bg
|
|
56
|
+
bg $panel
|
|
66
57
|
color $ink-soft
|
|
67
58
|
font-family inherit
|
|
68
59
|
font-size .8rem
|
|
69
60
|
cursor pointer
|
|
70
61
|
|
|
71
|
-
.hero
|
|
62
|
+
.hero pre .copy-cmd:hover
|
|
72
63
|
color $ink
|
|
73
64
|
border-color $accent
|
|
74
65
|
|
|
75
|
-
.hero
|
|
66
|
+
.hero pre .copy-cmd.is-copied
|
|
76
67
|
color $accent
|
|
77
68
|
border-color $accent
|
|
78
69
|
|
|
79
|
-
.hero-
|
|
80
|
-
display grid
|
|
81
|
-
grid-template-columns repeat(4, auto)
|
|
82
|
-
gap 1.25rem 2.5rem
|
|
83
|
-
list-style none
|
|
84
|
-
padding 0
|
|
85
|
-
margin 0
|
|
86
|
-
border-top 1px solid $line-soft
|
|
87
|
-
padding-top 1.5rem
|
|
88
|
-
|
|
89
|
-
.hero-stats li
|
|
70
|
+
.hero-cta
|
|
90
71
|
display flex
|
|
91
|
-
flex-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
font-size 1.6rem
|
|
95
|
-
color $accent
|
|
96
|
-
letter-spacing -.02em
|
|
97
|
-
|
|
98
|
-
.hero-stats span
|
|
99
|
-
font-size .82rem
|
|
100
|
-
color $muted
|
|
101
|
-
max-width 18ch
|
|
102
|
-
|
|
103
|
-
@media (max-width: 720px)
|
|
104
|
-
.hero-stats
|
|
105
|
-
grid-template-columns 1fr 1fr
|
|
106
|
-
|
|
107
|
-
/* ── vs-Markdoc comparison ──────────────────────────────────────── */
|
|
72
|
+
flex-wrap wrap
|
|
73
|
+
gap .6rem
|
|
74
|
+
margin 0
|
|
108
75
|
|
|
109
|
-
|
|
110
|
-
display grid
|
|
111
|
-
grid-template-columns 1fr 1fr
|
|
112
|
-
gap 1.25rem
|
|
113
|
-
align-items start
|
|
114
|
-
margin 2.5rem 0 1rem
|
|
76
|
+
/* ── Source + the same file running, as one paired module ───────── */
|
|
115
77
|
|
|
116
|
-
|
|
78
|
+
/* The source block and the live demo are deliberately the same width and sit
|
|
79
|
+
inside one bordered card, so the "this text produced that UI" claim reads as
|
|
80
|
+
a single before/after unit rather than two unrelated page chunks. */
|
|
81
|
+
.pair
|
|
82
|
+
max-width 46rem
|
|
83
|
+
margin 1.25rem 0 2.5rem
|
|
84
|
+
padding 1.25rem
|
|
117
85
|
border 1px solid $line
|
|
118
86
|
radius $radius
|
|
119
87
|
bg $panel
|
|
120
|
-
padding 1.35rem
|
|
121
|
-
display flex
|
|
122
|
-
flex-direction column
|
|
123
|
-
min-width 0
|
|
124
|
-
box-shadow 0 1px 2px rgba(28,24,20,.04)
|
|
125
|
-
|
|
126
|
-
.vs-card.vs-dark
|
|
127
|
-
background linear-gradient(180deg, $accent-soft, $panel)
|
|
128
|
-
border 1px solid $accent
|
|
129
88
|
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
justify-content space-between
|
|
134
|
-
gap .75rem
|
|
135
|
-
margin-bottom .85rem
|
|
136
|
-
|
|
137
|
-
.vs-tag
|
|
138
|
-
font-family ui-monospace, SFMono-Regular, Menlo, monospace
|
|
139
|
-
font-size .72rem
|
|
140
|
-
font-weight 700
|
|
89
|
+
.pairlabel
|
|
90
|
+
margin 0 0 .5rem
|
|
91
|
+
font-size .78rem
|
|
141
92
|
text-transform uppercase
|
|
142
|
-
letter-spacing .
|
|
143
|
-
padding .25rem .55rem
|
|
144
|
-
radius 999px
|
|
145
|
-
|
|
146
|
-
.vs-tag-wd
|
|
147
|
-
color white
|
|
148
|
-
bg $accent
|
|
149
|
-
|
|
150
|
-
.vs-tag-md
|
|
93
|
+
letter-spacing .07em
|
|
151
94
|
color $muted
|
|
152
|
-
bg $sunk
|
|
153
|
-
border 1px solid $line
|
|
154
95
|
|
|
155
|
-
.
|
|
156
|
-
font-
|
|
157
|
-
|
|
158
|
-
|
|
96
|
+
.pairlabel code
|
|
97
|
+
font-size .78rem
|
|
98
|
+
text-transform none
|
|
99
|
+
letter-spacing 0
|
|
100
|
+
color $ink
|
|
159
101
|
|
|
160
|
-
.
|
|
161
|
-
margin
|
|
162
|
-
font-size .8rem
|
|
163
|
-
line-height 1.55
|
|
102
|
+
.pair > pre
|
|
103
|
+
margin 0 0 1.5rem
|
|
164
104
|
|
|
165
|
-
.
|
|
166
|
-
margin
|
|
167
|
-
font-size .88rem
|
|
168
|
-
color $muted
|
|
169
|
-
font-style italic
|
|
105
|
+
.pair .pairlabel + .demo
|
|
106
|
+
margin 0
|
|
170
107
|
|
|
171
|
-
.
|
|
172
|
-
border 1px
|
|
108
|
+
.demo
|
|
109
|
+
border 1px solid $accent
|
|
173
110
|
radius $radius-sm
|
|
174
|
-
background color-mix(in srgb, $accent-soft
|
|
175
|
-
padding
|
|
176
|
-
margin .15rem 0 0
|
|
111
|
+
background color-mix(in srgb, $accent-soft 45%, $panel)
|
|
112
|
+
padding 1.15rem 1.25rem 1rem
|
|
177
113
|
|
|
178
|
-
.
|
|
114
|
+
.demo input[data-wd-bind-input]
|
|
179
115
|
width 100%
|
|
180
116
|
box-sizing border-box
|
|
181
117
|
font-family inherit
|
|
@@ -187,7 +123,7 @@
|
|
|
187
123
|
color $ink
|
|
188
124
|
margin 0 0 .75rem
|
|
189
125
|
|
|
190
|
-
.
|
|
126
|
+
.demo .product
|
|
191
127
|
display flex
|
|
192
128
|
align-items center
|
|
193
129
|
justify-content space-between
|
|
@@ -199,10 +135,10 @@
|
|
|
199
135
|
radius 8px
|
|
200
136
|
font-size .95rem
|
|
201
137
|
|
|
202
|
-
.
|
|
138
|
+
.demo .product p
|
|
203
139
|
margin 0
|
|
204
140
|
|
|
205
|
-
.
|
|
141
|
+
.demo .product button
|
|
206
142
|
border 0
|
|
207
143
|
bg $accent
|
|
208
144
|
color white
|
|
@@ -213,57 +149,68 @@
|
|
|
213
149
|
cursor pointer
|
|
214
150
|
flex-shrink 0
|
|
215
151
|
|
|
216
|
-
.
|
|
152
|
+
.demo .product button:hover
|
|
217
153
|
bg $accent-deep
|
|
218
154
|
|
|
219
|
-
.
|
|
155
|
+
.demo > p:last-of-type
|
|
220
156
|
margin .85rem 0 0
|
|
221
157
|
font-size .95rem
|
|
222
158
|
|
|
223
|
-
|
|
224
|
-
margin 1rem 0 0
|
|
225
|
-
padding 1rem 1.1rem
|
|
226
|
-
border-left 3px solid $warm
|
|
227
|
-
bg $warm-soft
|
|
228
|
-
radius 8px
|
|
159
|
+
/* ── Live data strip (:fetch stars + visits) ────────────────────── */
|
|
229
160
|
|
|
230
|
-
.
|
|
231
|
-
|
|
232
|
-
|
|
161
|
+
.livestrip
|
|
162
|
+
display grid
|
|
163
|
+
grid-template-columns repeat(4, 1fr)
|
|
164
|
+
gap 1rem
|
|
165
|
+
margin 1.5rem 0 2rem
|
|
166
|
+
padding 1.35rem 0
|
|
167
|
+
border-top 1px solid $line-soft
|
|
168
|
+
border-bottom 1px solid $line-soft
|
|
169
|
+
|
|
170
|
+
/* Every stat is structurally identical: the value is a bare <p> (either plain
|
|
171
|
+
prose or the body of an `:if` region), the label is its own `.statlabel`
|
|
172
|
+
container. Keeping the label in a container is what stops markdown from
|
|
173
|
+
folding value and label into one paragraph, which is exactly what happened
|
|
174
|
+
when they were two adjacent lines. */
|
|
175
|
+
.stat
|
|
176
|
+
display flex
|
|
177
|
+
flex-direction column
|
|
178
|
+
gap .2rem
|
|
179
|
+
min-width 0
|
|
233
180
|
|
|
234
|
-
.
|
|
235
|
-
margin
|
|
236
|
-
font-size .
|
|
181
|
+
.stat > p
|
|
182
|
+
margin 0
|
|
183
|
+
font-size 1.85rem
|
|
184
|
+
line-height 1.05
|
|
185
|
+
color $accent
|
|
186
|
+
letter-spacing -.02em
|
|
187
|
+
font-variant-numeric tabular-nums
|
|
237
188
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
189
|
+
/* The `:if` lifecycle regions wrap the value; unwrap them for layout so the
|
|
190
|
+
value sits on the same baseline whether it is static or fetched. */
|
|
191
|
+
.stat > div[data-wd-if] p
|
|
192
|
+
margin 0
|
|
193
|
+
font-size 1.85rem
|
|
194
|
+
line-height 1.05
|
|
195
|
+
color $accent
|
|
196
|
+
letter-spacing -.02em
|
|
197
|
+
font-variant-numeric tabular-nums
|
|
242
198
|
|
|
243
|
-
.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
font-size .95rem
|
|
199
|
+
.statlabel
|
|
200
|
+
font-size .88rem
|
|
201
|
+
line-height 1.35
|
|
202
|
+
color $ink-soft
|
|
203
|
+
max-width 20ch
|
|
249
204
|
|
|
250
|
-
.
|
|
251
|
-
|
|
205
|
+
.statlabel p
|
|
206
|
+
margin 0
|
|
252
207
|
|
|
253
208
|
@media (max-width: 720px)
|
|
254
|
-
.
|
|
255
|
-
grid-template-columns 1fr
|
|
256
|
-
gap
|
|
257
|
-
|
|
258
|
-
/* ── Secondary value props ──────────────────────────────────────── */
|
|
259
|
-
|
|
260
|
-
.props
|
|
261
|
-
margin 1.5rem 0 0
|
|
262
|
-
|
|
263
|
-
.props h2
|
|
264
|
-
margin-top 1.5rem
|
|
209
|
+
.livestrip
|
|
210
|
+
grid-template-columns 1fr 1fr
|
|
211
|
+
gap 1.25rem
|
|
265
212
|
|
|
266
|
-
/* ── Demo-directive widgets (`:try
|
|
213
|
+
/* ── Demo-directive widgets (`:try`) ────────────────────────────── */
|
|
267
214
|
|
|
268
215
|
.try-card
|
|
269
216
|
display inline-flex
|
|
@@ -286,44 +233,3 @@
|
|
|
286
233
|
padding .1rem .5rem
|
|
287
234
|
radius 999px
|
|
288
235
|
font-size .8rem
|
|
289
|
-
|
|
290
|
-
.feature-card
|
|
291
|
-
display grid
|
|
292
|
-
gap .35rem
|
|
293
|
-
margin 1rem 0
|
|
294
|
-
padding 1.1rem 1.25rem
|
|
295
|
-
bg $panel
|
|
296
|
-
border 1px solid $line
|
|
297
|
-
radius $radius
|
|
298
|
-
|
|
299
|
-
.feature-card strong
|
|
300
|
-
color $warm
|
|
301
|
-
|
|
302
|
-
.pulse-button
|
|
303
|
-
border 0
|
|
304
|
-
bg $ink
|
|
305
|
-
color $paper
|
|
306
|
-
padding .85rem 1.15rem
|
|
307
|
-
radius 999px
|
|
308
|
-
font-family inherit
|
|
309
|
-
cursor pointer
|
|
310
|
-
|
|
311
|
-
.pulse-button.is-on
|
|
312
|
-
bg $warm
|
|
313
|
-
|
|
314
|
-
.sprint-board
|
|
315
|
-
display grid
|
|
316
|
-
grid-template-columns repeat(auto-fit, minmax(130px, 1fr))
|
|
317
|
-
gap 1rem
|
|
318
|
-
margin 1rem 0
|
|
319
|
-
|
|
320
|
-
.sprint-board article
|
|
321
|
-
bg $panel
|
|
322
|
-
border 1px solid $line
|
|
323
|
-
padding 1rem
|
|
324
|
-
radius $radius
|
|
325
|
-
|
|
326
|
-
.sprint-board span
|
|
327
|
-
display block
|
|
328
|
-
color $accent
|
|
329
|
-
font-size .9rem
|
package/site/pages/index.wd
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
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.
|
|
2
|
+
title: Darkmown: Markdown that runs
|
|
3
|
+
description: A .md file is a page. Rename it to .wd and the same file gets loops, state, fetched data, and forms, with no components, no JSX, and no build step. Pages with no state ship zero framework JavaScript.
|
|
5
4
|
image: https://darkmown.com/__wd/media/og.png
|
|
6
5
|
site_url: https://darkmown.com
|
|
7
6
|
transitions: true
|
|
@@ -10,39 +9,43 @@ transitions: true
|
|
|
10
9
|
@include /nav.wd
|
|
11
10
|
@include /base.wd
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
::: main
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
<span class="eyebrow">Darkmown vs. Markdoc</span>
|
|
14
|
+
::: hero
|
|
17
15
|
|
|
18
|
-
# Markdown that *runs.*
|
|
16
|
+
# Markdown that *runs.*
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
A `.md` file is already a page. Rename it to `.wd` and that same file gets loops, state, fetched data, and forms. No components, no JSX, no config. Pages with no state ship **zero** framework JavaScript.
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</div>
|
|
20
|
+
```sh
|
|
21
|
+
npx @zvndev/darkmown init my-site
|
|
22
|
+
```
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
::: hero-cta
|
|
25
|
+
[Read the docs](/docs/){.btn .primary}
|
|
26
|
+
[Try it in your browser](/playground/){.btn}
|
|
27
|
+
[Star on GitHub](https://github.com/ZVN-DEV/darkmown){.btn}
|
|
28
|
+
:::
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
:::
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
<li><strong>0 KB</strong><span>framework JS on static pages</span></li>
|
|
33
|
-
<li><strong>~7.7 KB</strong><span>one runtime, CI-enforced < 8 KB</span></li>
|
|
34
|
-
<li><strong>1</strong><span>loop and one binding to learn</span></li>
|
|
35
|
-
<li><strong>MIT</strong><span>zero runtime dependencies</span></li>
|
|
36
|
-
</ul>
|
|
37
|
-
</section>
|
|
32
|
+
## One file. Here is the source.
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
A store with a live search filter, add-to-cart, and a running total.
|
|
40
35
|
|
|
41
|
-
:::
|
|
42
|
-
|
|
36
|
+
::: pair
|
|
37
|
+
|
|
38
|
+
::: pairlabel
|
|
39
|
+
`store.wd`, the whole file
|
|
40
|
+
:::
|
|
43
41
|
|
|
44
42
|
```wd
|
|
45
|
-
:state products = [
|
|
43
|
+
:state products = [
|
|
44
|
+
{ "id": 1, "name": "Aurora Lamp", "price": 49 },
|
|
45
|
+
{ "id": 2, "name": "Briza Fan", "price": 39 },
|
|
46
|
+
{ "id": 3, "name": "Cove Speaker", "price": 89 },
|
|
47
|
+
{ "id": 4, "name": "Dune Mug", "price": 12 }
|
|
48
|
+
]
|
|
46
49
|
:state cart = []
|
|
47
50
|
:state q = ""
|
|
48
51
|
:computed count = cart.length
|
|
@@ -51,7 +54,7 @@ The left file is Markdown. It is also a running app — a live search filter, ad
|
|
|
51
54
|
|
|
52
55
|
@loop products into p where p.name contains q
|
|
53
56
|
::: card .product
|
|
54
|
-
**{ p.name }**
|
|
57
|
+
**{ p.name }** · ${ p.price }
|
|
55
58
|
:button "Add" -> cart += p
|
|
56
59
|
:::
|
|
57
60
|
@endloop
|
|
@@ -59,10 +62,17 @@ The left file is Markdown. It is also a running app — a live search filter, ad
|
|
|
59
62
|
Cart: { count } item(s)
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
::: pairlabel
|
|
66
|
+
that same file, running (type in the box)
|
|
67
|
+
:::
|
|
63
68
|
|
|
64
|
-
:::
|
|
65
|
-
:state products = [
|
|
69
|
+
::: demo
|
|
70
|
+
:state products = [
|
|
71
|
+
{ "id": 1, "name": "Aurora Lamp", "price": 49 },
|
|
72
|
+
{ "id": 2, "name": "Briza Fan", "price": 39 },
|
|
73
|
+
{ "id": 3, "name": "Cove Speaker", "price": 89 },
|
|
74
|
+
{ "id": 4, "name": "Dune Mug", "price": 12 }
|
|
75
|
+
]
|
|
66
76
|
:state cart = []
|
|
67
77
|
:state q = ""
|
|
68
78
|
:computed count = cart.length
|
|
@@ -71,7 +81,7 @@ Cart: { count } item(s)
|
|
|
71
81
|
|
|
72
82
|
@loop products into p where p.name contains q
|
|
73
83
|
::: card .product
|
|
74
|
-
**{ p.name }**
|
|
84
|
+
**{ p.name }** · ${ p.price }
|
|
75
85
|
:button "Add" -> cart += p
|
|
76
86
|
:::
|
|
77
87
|
@endloop
|
|
@@ -79,102 +89,104 @@ Cart: { count } item(s)
|
|
|
79
89
|
Cart: **{ count }** item(s)
|
|
80
90
|
:::
|
|
81
91
|
|
|
82
|
-
<p class="vs-caption">This is the whole file. It runs.</p>
|
|
83
92
|
:::
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
<header class="vs-head"><span class="vs-tag vs-tag-md">Markdoc</span><span class="vs-sub">store.md</span></header>
|
|
87
|
-
|
|
88
|
-
```text
|
|
89
|
-
{% if $products %}
|
|
90
|
-
- **Aurora Lamp** — $49
|
|
91
|
-
- **Briza Fan** — $39
|
|
92
|
-
- **Cove Speaker** — $89
|
|
93
|
-
- **Dune Mug** — $12
|
|
94
|
-
{% /if %}
|
|
95
|
-
```
|
|
94
|
+
## This page is built in Darkmown
|
|
96
95
|
|
|
97
|
-
|
|
96
|
+
Not a screenshot, and not a React site describing a Markdown tool. `site/pages/index.wd` is a `.wd` file with no raw HTML in it. The first two numbers below are live: a `:fetch` reading `api/stats.js`, a serverless function in this same repo.
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
<strong>+ store-widget.jsx</strong> — a React component, registered as a custom tag:
|
|
98
|
+
:fetch stats from "/api/stats" timeout=6000
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
const tags = { store: { render: 'Store' } };
|
|
104
|
-
Markdoc.renderers.react(content, React, {
|
|
105
|
-
components: { Store } // your filter + cart + total live here
|
|
106
|
-
});
|
|
107
|
-
```
|
|
100
|
+
::: livestrip
|
|
108
101
|
|
|
109
|
-
|
|
102
|
+
::: stat
|
|
103
|
+
:if stats_loading
|
|
104
|
+
·
|
|
105
|
+
:endif
|
|
106
|
+
:if stats_error
|
|
107
|
+
n/a
|
|
108
|
+
:endif
|
|
109
|
+
:if stats
|
|
110
|
+
{ stats.version }
|
|
111
|
+
:endif
|
|
112
|
+
::: statlabel
|
|
113
|
+
latest release, live from npm
|
|
110
114
|
:::
|
|
111
|
-
|
|
112
|
-
<p class="vs-caption">Same outcome needs a React component.</p>
|
|
113
115
|
:::
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
## A whole framework, in text
|
|
117
|
+
::: stat
|
|
118
|
+
:if stats_loading
|
|
119
|
+
·
|
|
120
|
+
:endif
|
|
121
|
+
:if stats_error
|
|
122
|
+
n/a
|
|
123
|
+
:endif
|
|
124
|
+
:if stats
|
|
125
|
+
{ stats.downloads }
|
|
126
|
+
:endif
|
|
127
|
+
::: statlabel
|
|
128
|
+
npm installs last month
|
|
129
|
+
:::
|
|
130
|
+
:::
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
::: stat
|
|
133
|
+
7,654 B
|
|
134
|
+
::: statlabel
|
|
135
|
+
the whole runtime, gzipped
|
|
136
|
+
:::
|
|
137
|
+
:::
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
::: stat
|
|
140
|
+
0 KB
|
|
141
|
+
::: statlabel
|
|
142
|
+
JS on a static page
|
|
143
|
+
:::
|
|
144
|
+
:::
|
|
138
145
|
|
|
139
|
-
|
|
146
|
+
:::
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
:try "The docs route — a .wd page built from includes" href="/docs/"
|
|
143
|
-
:try "A plain .md page where directives stay inert text" href="/markdown/"
|
|
144
|
-
:try "Reactive page: state, keyed loops, scoped sections" href="/reactive/"
|
|
145
|
-
:try "Data page: fetch, forms, server round-trips, persistence" href="/data/"
|
|
146
|
-
:try "A hidden route, to confirm it 404s" href="/-draft/"
|
|
148
|
+
Say hello and the form round-trips to a real serverless function, then renders the reply:
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
:form action="/api/echo" into reply
|
|
151
|
+
:input note placeholder="What would you build with this?"
|
|
152
|
+
:submit "Send it"
|
|
153
|
+
:endform
|
|
149
154
|
|
|
150
|
-
|
|
155
|
+
:if reply
|
|
156
|
+
The server answered at **{ reply.at }**. That was a `:form action=`. With JavaScript off it degrades to a plain native POST.
|
|
157
|
+
:else if reply_error
|
|
158
|
+
The echo endpoint is only live on the deployed site: { reply_error }
|
|
159
|
+
:endif
|
|
151
160
|
|
|
152
|
-
|
|
153
|
-
@loop /features.json into card
|
|
154
|
-
@include /feature-card.wd
|
|
155
|
-
@endloop
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
@loop /features.json into card
|
|
159
|
-
@include /feature-card.wd
|
|
160
|
-
@endloop
|
|
161
|
+
## Three rules, no escape hatch
|
|
161
162
|
|
|
162
|
-
|
|
163
|
+
- **Zero-JS static.** A page with no state ships no framework JavaScript. Open [the docs](/docs/) or [a plain `.md` page](/markdown/) and check the network tab.
|
|
164
|
+
- **One runtime.** Every reactive page shares the same 7,654-byte gzipped runtime, CI-enforced under 8 KB. No per-component bundles, no hydration tax that grows with your UI. It runs under a strict CSP with no `unsafe-eval`.
|
|
165
|
+
- **One loop, one binding.** `@loop … into …` is the only loop. `{ name }` is the only interpolation. There is no second way to do either.
|
|
163
166
|
|
|
164
|
-
|
|
167
|
+
## A whole framework, in text
|
|
165
168
|
|
|
166
|
-
|
|
169
|
+
- **Routing.** Folders are routes. Drop a file in, it is a page. Prefix it with `.` or `-` to hide it.
|
|
170
|
+
- **State and reactivity.** `:state`, `:button`, `:if`, and keyed `@loop`s patch the DOM directly. No virtual DOM.
|
|
171
|
+
- **Server calls.** `:fetch` pulls JSON into state; `:form action=` round-trips to any backend and degrades to a native POST without JS.
|
|
172
|
+
- **Composition.** `@include` embeds one Markdown file into another, passing values down.
|
|
173
|
+
- **Styling, optional.** Colocate a `.skin` file for design tokens, or ship none and let the browser default.
|
|
174
|
+
- **Errors that teach.** Every compile error carries a stable code, the `file:line`, and a corrective example that is guaranteed to compile.
|
|
167
175
|
|
|
168
|
-
|
|
176
|
+
## See it for real
|
|
169
177
|
|
|
170
|
-
|
|
178
|
+
:try "A whole live app in one file: fetch, loops, nested ifs, a form" href="/app/"
|
|
179
|
+
:try "The docs, a .wd page built from includes" href="/docs/"
|
|
180
|
+
:try "A plain .md page, where directives stay inert text" href="/markdown/"
|
|
171
181
|
|
|
172
|
-
`npm run build` emits a plain static `dist/`
|
|
182
|
+
`npm run build` emits a plain static `dist/` (no server, no special runtime), so it deploys anywhere that serves files. This site is a Darkmown build.
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
::: hero-cta
|
|
185
|
+
[Deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FZVN-DEV%2Fdarkmown){.btn .primary}
|
|
186
|
+
[Read the docs](/docs/){.btn}
|
|
187
|
+
[How it compares to Markdoc](/vs/markdoc/){.btn}
|
|
188
|
+
:::
|
|
177
189
|
|
|
178
|
-
|
|
190
|
+
:::
|
|
179
191
|
|
|
180
192
|
@include /footer.wd
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* The Darkmown vs. Markdoc comparison, moved off the landing page.
|
|
2
|
+
Layers on /base.wd. Class names match the `::: name .class` containers
|
|
3
|
+
in markdoc.wd. The page contains no raw HTML. */
|
|
4
|
+
|
|
5
|
+
.versus
|
|
6
|
+
display grid
|
|
7
|
+
grid-template-columns 1fr 1fr
|
|
8
|
+
gap 1.25rem
|
|
9
|
+
align-items start
|
|
10
|
+
margin 2.75rem 0 2.5rem
|
|
11
|
+
|
|
12
|
+
.vs-card
|
|
13
|
+
border 1px solid $line
|
|
14
|
+
radius $radius
|
|
15
|
+
bg $panel
|
|
16
|
+
padding 1.35rem
|
|
17
|
+
display flex
|
|
18
|
+
flex-direction column
|
|
19
|
+
min-width 0
|
|
20
|
+
box-shadow 0 1px 2px rgba(28,24,20,.04)
|
|
21
|
+
|
|
22
|
+
.vs-card.vs-dark
|
|
23
|
+
background linear-gradient(180deg, $accent-soft, $panel)
|
|
24
|
+
border 1px solid $accent
|
|
25
|
+
|
|
26
|
+
/* First line of each card is the filename, authored as inline code. */
|
|
27
|
+
.vs-card > p:first-child
|
|
28
|
+
margin 0 0 .85rem
|
|
29
|
+
font-size .82rem
|
|
30
|
+
color $muted
|
|
31
|
+
|
|
32
|
+
.vs-card > p:first-child code
|
|
33
|
+
font-size .82rem
|
|
34
|
+
color $ink
|
|
35
|
+
|
|
36
|
+
.vs-card pre
|
|
37
|
+
margin .25rem 0 .75rem
|
|
38
|
+
padding-right 1rem
|
|
39
|
+
font-size .78rem
|
|
40
|
+
line-height 1.55
|
|
41
|
+
overflow-x auto
|
|
42
|
+
|
|
43
|
+
.vs-card p
|
|
44
|
+
font-size .92rem
|
|
45
|
+
|
|
46
|
+
.vs-card > p:last-child
|
|
47
|
+
margin auto 0 0
|
|
48
|
+
padding-top .5rem
|
|
49
|
+
|
|
50
|
+
@media (max-width: 720px)
|
|
51
|
+
.versus
|
|
52
|
+
grid-template-columns 1fr
|
|
53
|
+
gap 1rem
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Darkmown vs. Markdoc: Markdown that runs, without a React component
|
|
3
|
+
description: Markdoc gives Markdown conditionals, partials and variables, but no loop tag and no interactivity. A filter, a cart, or a total means writing a React component and shipping a hydration runtime. Darkmown does it in the Markdown file. Side by side.
|
|
4
|
+
image: https://darkmown.com/__wd/media/og.png
|
|
5
|
+
site_url: https://darkmown.com
|
|
6
|
+
transitions: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
@include /nav.wd
|
|
10
|
+
@include /base.wd
|
|
11
|
+
|
|
12
|
+
::: main
|
|
13
|
+
|
|
14
|
+
# Darkmown vs. Markdoc
|
|
15
|
+
|
|
16
|
+
[Markdoc](https://markdoc.dev) is a good tool, and the closest thing to Darkmown in spirit: Markdown with first-party syntax instead of MDX's "Markdown plus JavaScript". If you are choosing between them, the difference is narrow and specific.
|
|
17
|
+
|
|
18
|
+
**Markdoc renders content. Darkmown runs it.** Markdoc has `{% if %}`, `{% partial %}` and `$variables`, so it can vary what it renders at build time. It has no loop tag, and no state, so the moment your page needs a filter, a cart, or a running total, you register a custom tag backed by a React component, ship React and a hydration runtime to the browser, and wire it up in your build. Darkmown expresses those in the Markdown file, and ships one 7,654-byte runtime.
|
|
19
|
+
|
|
20
|
+
## The same store, both ways
|
|
21
|
+
|
|
22
|
+
::: versus
|
|
23
|
+
|
|
24
|
+
::: card .vs-card .vs-dark
|
|
25
|
+
`store.wd` (Darkmown)
|
|
26
|
+
|
|
27
|
+
```wd
|
|
28
|
+
:state products = [
|
|
29
|
+
{ "id": 1, "name": "Aurora Lamp", "price": 49 },
|
|
30
|
+
{ "id": 2, "name": "Briza Fan", "price": 39 }
|
|
31
|
+
]
|
|
32
|
+
:state cart = []
|
|
33
|
+
:state q = ""
|
|
34
|
+
:computed count = cart.length
|
|
35
|
+
|
|
36
|
+
:bind q placeholder="Search products…"
|
|
37
|
+
|
|
38
|
+
@loop products into p where p.name contains q
|
|
39
|
+
::: card .product
|
|
40
|
+
**{ p.name }** · ${ p.price }
|
|
41
|
+
:button "Add" -> cart += p
|
|
42
|
+
:::
|
|
43
|
+
@endloop
|
|
44
|
+
|
|
45
|
+
Cart: { count } item(s)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That is the whole file. [It is running on the homepage](/): search, add to cart, and a live total.
|
|
49
|
+
:::
|
|
50
|
+
|
|
51
|
+
::: card .vs-card
|
|
52
|
+
`store.md` (Markdoc)
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
{% if $products %}
|
|
56
|
+
- **Aurora Lamp**, $49
|
|
57
|
+
- **Briza Fan**, $39
|
|
58
|
+
{% /if %}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
There is no loop tag, so each row is written by hand and the list is static. To filter, add to a cart, or total it, you reach for code:
|
|
62
|
+
|
|
63
|
+
```jsx
|
|
64
|
+
const tags = {
|
|
65
|
+
store: { render: 'Store' }
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// your filter + cart + total live here
|
|
69
|
+
Markdoc.renderers.react(content, React, {
|
|
70
|
+
components: { Store }
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
…plus React and a hydration runtime shipped to the browser, and the build step that wires the tag to the component.
|
|
75
|
+
:::
|
|
76
|
+
|
|
77
|
+
:::
|
|
78
|
+
|
|
79
|
+
## Where each one wins
|
|
80
|
+
|
|
81
|
+
- **Pick Markdoc** if you are already in a React app, want Markdown authoring with a validated custom-tag schema, and are happy for interactivity to live in React components. Its schema validation and editor tooling are genuinely strong, and it is battle-tested at Stripe's scale.
|
|
82
|
+
- **Pick Darkmown** if you want the page itself to be the app (loops, state, fetch, and forms expressed in Markdown), and you want static pages to ship literally zero framework JavaScript.
|
|
83
|
+
|
|
84
|
+
## Honest differences
|
|
85
|
+
|
|
86
|
+
- **Markdoc is older and more proven.** It runs Stripe's documentation. Darkmown is young; judge it accordingly.
|
|
87
|
+
- **Markdoc has schema validation for custom tags.** Darkmown has no custom tags at all, by design: the directive vocabulary is fixed and closed, which is what lets it validate expressions at compile time and run without `eval`.
|
|
88
|
+
- **Markdoc is renderer-agnostic** (React, HTML, or your own). Darkmown compiles to static HTML plus its own small runtime, and is not a library you embed in another framework's render tree.
|
|
89
|
+
- **Neither ships a component model.** Markdoc composes with `{% partial %}`, Darkmown with `@include`.
|
|
90
|
+
|
|
91
|
+
[Read the docs](/docs/){.btn .primary}
|
|
92
|
+
[Try Darkmown in your browser](/playground/){.btn}
|
|
93
|
+
|
|
94
|
+
:::
|
|
95
|
+
|
|
96
|
+
@include /footer.wd
|