@zvndev/darkmown 2.6.1-next.11.g82bb8d7 → 2.7.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 +392 -77
- package/CHANGELOG.md +47 -0
- package/README.md +316 -49
- package/docs/cli.md +51 -17
- package/docs/spec-alignment.md +25 -4
- package/grammar/wd-directives.gbnf +13 -5
- package/package.json +7 -3
- package/site/_/base.skin +1 -1
- package/site/_/footer.wd +1 -1
- package/site/pages/api-errors/index.wd +81 -0
- package/site/pages/app.wd +1 -1
- package/site/pages/attr-binding/index.wd +84 -0
- package/site/pages/blog/darkmown-2-7-0.md +176 -0
- package/site/pages/blog/darkmown-2-7-0.skin +127 -0
- package/site/pages/blog/no-unsafe-eval.md +66 -0
- package/site/pages/blog/no-unsafe-eval.skin +127 -0
- package/site/pages/blog/zero-js-by-default.md +1 -1
- package/site/pages/branch-state/index.wd +68 -0
- package/site/pages/docs/index.wd +320 -19
- package/site/pages/index.wd +3 -1
- package/site/pages/playground/index.js +63 -56
- package/site/pages/pulse/index.wd +1 -1
- package/site/pages/settings/index.wd +97 -0
- package/site/pages/showcase/index.wd +37 -1
- package/site/pages/upload/index.wd +63 -0
- package/site/pages/url-state/index.wd +70 -0
- package/src/builder.js +222 -34
- package/src/catalog.js +217 -21
- package/src/cli.js +6 -3
- package/src/compiler/actions.js +80 -12
- package/src/compiler/body.js +104 -28
- package/src/compiler/collections.js +29 -13
- package/src/compiler/context.js +9 -2
- package/src/compiler/expr-ast.js +14 -0
- package/src/compiler/fetch.js +8 -4
- package/src/compiler/forms.js +133 -14
- package/src/compiler/frontmatter.js +28 -7
- package/src/compiler/includes.js +46 -6
- package/src/compiler/interpolation.js +56 -1
- package/src/compiler/loops.js +489 -35
- package/src/compiler/markdown.js +457 -44
- package/src/compiler/page.js +8 -3
- package/src/compiler/predicates.js +84 -14
- package/src/compiler/state.js +167 -18
- package/src/compiler/structure.js +159 -29
- package/src/errors.js +111 -3
- package/src/feeds.js +140 -7
- package/src/grammar.js +53 -10
- package/src/runtime.js +509 -130
- package/src/runtime.min.js +2 -0
- package/src/runtime.min.js.map +1 -0
- package/src/statics.js +46 -2
- package/src/templates/blog/README.md +2 -0
- package/src/templates/dashboard/README.md +2 -0
- package/src/templates/landing/README.md +2 -0
- package/src/templates/starter/README.md +1 -1
- package/src/templates/store/README.md +2 -0
- package/src/tools/validate.js +14 -4
- package/types/builder.d.ts +6 -2
- package/types/catalog.d.ts +12 -0
- package/types/compiler/actions.d.ts +1 -1
- package/types/compiler/body.d.ts +0 -3
- package/types/compiler/forms.d.ts +12 -1
- package/types/compiler/frontmatter.d.ts +8 -1
- package/types/compiler/includes.d.ts +16 -2
- package/types/compiler/interpolation.d.ts +23 -0
- package/types/compiler/loops.d.ts +47 -0
- package/types/compiler/markdown.d.ts +63 -1
- package/types/compiler/predicates.d.ts +7 -1
- package/types/compiler/state.d.ts +8 -2
- package/types/compiler/structure.d.ts +32 -1
- package/types/feeds.d.ts +68 -2
- package/types/statics.d.ts +25 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Darkmown 2.7.0: no more silent failures"
|
|
3
|
+
description: A release built around one question asked of every code path, when this goes wrong does the author find out. Plus a minified runtime that paid for five new features.
|
|
4
|
+
date: 2026-09-04
|
|
5
|
+
excerpt: A brace in a link destination used to stay literal text with no warning. A line that was just ":state" rendered as prose. Both are fixed, and the runtime got small enough to afford five new features.
|
|
6
|
+
schema: BlogPosting
|
|
7
|
+
tags: [release, compiler, runtime]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Darkmown 2.7.0: no more silent failures
|
|
11
|
+
|
|
12
|
+
Most framework bugs announce themselves. You get a stack trace, a red screen,
|
|
13
|
+
a test that goes from green to red. The bugs worth being afraid of are the
|
|
14
|
+
other kind: the page compiles, the build says `Built 38 routes`, and something
|
|
15
|
+
on it is quietly wrong.
|
|
16
|
+
|
|
17
|
+
This release came out of an audit that asked one question of every path in the
|
|
18
|
+
compiler, the builder and the runtime. **When this goes wrong, does the author
|
|
19
|
+
find out?** Too often the honest answer was no.
|
|
20
|
+
|
|
21
|
+
Four examples, all of which shipped in 2.6.0 and none of which printed a word:
|
|
22
|
+
|
|
23
|
+
- `[Read more](/docs/{ post.slug }/)` left the literal text `/docs/{ post.slug }/`
|
|
24
|
+
in the `href`. The brace resolved only when it filled the entire destination.
|
|
25
|
+
- A line that was exactly `:state`, with no name and no value, rendered as the
|
|
26
|
+
text `:state` on the page.
|
|
27
|
+
- A static `@loop` over `| … |` table rows produced a header-only table
|
|
28
|
+
followed by three paragraphs reading `| Ash | 3 | 12 |`.
|
|
29
|
+
- A `:every 5s -> n++` inside a reactive `@loop` registered one interval per
|
|
30
|
+
row, and a row removed from the list kept ticking forever.
|
|
31
|
+
|
|
32
|
+
Every one of those is now either compiled correctly or refused with a coded
|
|
33
|
+
error that names the fix. That is the whole shape of the release.
|
|
34
|
+
|
|
35
|
+
## Braces resolve, and then they bind
|
|
36
|
+
|
|
37
|
+
The destination case turned out to be worth more than a bug fix. Once every
|
|
38
|
+
brace in a destination resolves, the obvious next question is what happens when
|
|
39
|
+
the value is reactive. It binds:
|
|
40
|
+
|
|
41
|
+
```wd
|
|
42
|
+
:state region = "eu"
|
|
43
|
+
|
|
44
|
+
:button "EU" -> region = "eu"
|
|
45
|
+
:button "US" -> region = "us"
|
|
46
|
+
|
|
47
|
+
[Open the docs for this region](/docs/{ region }/)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Click a button and the `href` changes. The compiler emits the destination as a
|
|
51
|
+
small template (the literal parts plus a reader) and the runtime rebuilds the
|
|
52
|
+
whole value on every render. The build-time paint is still the seed, so the
|
|
53
|
+
link works before the runtime loads and a crawler following it lands somewhere
|
|
54
|
+
real.
|
|
55
|
+
|
|
56
|
+
Two guards came with it. A value in URL position is percent-encoded, so a `)`
|
|
57
|
+
arriving from your data cannot close the link and hand the rest back to the
|
|
58
|
+
Markdown parser. And the runtime re-checks the scheme of the value it
|
|
59
|
+
assembled, refusing `javascript:`, `data:` and `vbscript:` with an empty
|
|
60
|
+
attribute rather than a half-applied one, because the compiler cannot see a
|
|
61
|
+
value that only exists at runtime.
|
|
62
|
+
|
|
63
|
+
Raw HTML attributes still do not bind. `<a href="{ url }">` is painted once,
|
|
64
|
+
and now says so in a warning that names the position that does bind.
|
|
65
|
+
|
|
66
|
+
## The URL is state
|
|
67
|
+
|
|
68
|
+
Here is the feature I have wanted longest. A filter nobody can link to is half
|
|
69
|
+
a feature:
|
|
70
|
+
|
|
71
|
+
```wd
|
|
72
|
+
:state q = "" from-url
|
|
73
|
+
|
|
74
|
+
:bind q placeholder="Search products"
|
|
75
|
+
|
|
76
|
+
:state products = [{"name": "Aurora Lamp"}, {"name": "Briza Fan"}]
|
|
77
|
+
|
|
78
|
+
@loop products into p where p.name contains q
|
|
79
|
+
- **{ p.name }**
|
|
80
|
+
@empty
|
|
81
|
+
Nothing matches that search.
|
|
82
|
+
@endloop
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Type in the box and the address bar becomes `?q=aurora`. Reload and the search
|
|
86
|
+
comes back. Send the URL to somebody and they open on the same view.
|
|
87
|
+
|
|
88
|
+
`from-url` is a third word in the persistence vocabulary, next to `persist` and
|
|
89
|
+
`ephemeral`, and it is on `:state` only. Writes go through
|
|
90
|
+
`history.replaceState`, so filtering never fills the back button with one entry
|
|
91
|
+
per keystroke. A value back at its declared seed drops its parameter, so the
|
|
92
|
+
default page keeps a clean URL. Combine it with `persist` and the precedence is
|
|
93
|
+
URL, then storage, then seed: a link somebody sent you beats what this browser
|
|
94
|
+
remembers.
|
|
95
|
+
|
|
96
|
+
Putting it on a `:store` is a compile error rather than a guess, and the error
|
|
97
|
+
says why. A `:store` is shared by every page and every tab, while a query
|
|
98
|
+
parameter belongs to one page's address. Those cannot mean the same thing.
|
|
99
|
+
|
|
100
|
+
## What the API actually said
|
|
101
|
+
|
|
102
|
+
A failed `:fetch` used to set `name_error` to `Error: HTTP 422`, no matter what
|
|
103
|
+
the server had gone to the trouble of explaining. Now, when the response body
|
|
104
|
+
is JSON, `name_error` is the body's own `error` or `message` field, and the
|
|
105
|
+
whole parsed body lands in a new `name_error_body`:
|
|
106
|
+
|
|
107
|
+
```wd
|
|
108
|
+
:fetch signup from "/api/signup" method=POST
|
|
109
|
+
|
|
110
|
+
:if signup_error
|
|
111
|
+
**{ signup_error }**
|
|
112
|
+
:if signup_error_body
|
|
113
|
+
Photo: { signup_error_body.fields.photo }
|
|
114
|
+
:endif
|
|
115
|
+
:endif
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Given a 422 whose body is
|
|
119
|
+
`{"error": "Pick a file first.", "fields": {"photo": "No file was attached."}}`,
|
|
120
|
+
the page renders *Pick a file first.* and the per-field message underneath it.
|
|
121
|
+
No JavaScript of your own.
|
|
122
|
+
|
|
123
|
+
## Smaller runtime, more features
|
|
124
|
+
|
|
125
|
+
None of the above would have fit. The runtime shipped as comment-stripped
|
|
126
|
+
readable source, and the audit fixes alone had taken it to 8036 bytes gzipped
|
|
127
|
+
out of a budget of 8192. Adding the five features below would have put the same
|
|
128
|
+
file at 8886, well over the line.
|
|
129
|
+
|
|
130
|
+
It now ships minified: a committed esbuild artifact, with an external sourcemap
|
|
131
|
+
served next to it so DevTools still shows the readable source with real names
|
|
132
|
+
and real comments. That bought about 2 KB back, and this release spent it:
|
|
133
|
+
attribute binding, `from-url`, bound `:select`/`:radio`/`:checkbox` outside a
|
|
134
|
+
form, server error bodies, and real multipart file upload. Total after all five:
|
|
135
|
+
**6584 bytes gzipped**, against the same 8192 byte budget.
|
|
136
|
+
|
|
137
|
+
The budget did not move. That is the point of having one.
|
|
138
|
+
|
|
139
|
+
## Things that now fail loudly
|
|
140
|
+
|
|
141
|
+
Some of the audit findings could not be fixed by making them work, because
|
|
142
|
+
there was nothing correct to do. Those became errors:
|
|
143
|
+
|
|
144
|
+
- **`WD191`**, a reactive `@loop` over Markdown table rows. A reactive row is
|
|
145
|
+
cloned into a `<div>`, which is not a legal child of `<table>`. The hint names
|
|
146
|
+
both ways out: a static source, or containers styled as table rows.
|
|
147
|
+
- **`WD315`**, `:every` or `:effect` inside a reactive loop body. Neither has a
|
|
148
|
+
per-row meaning to salvage.
|
|
149
|
+
- **A bare directive keyword**, which now throws that directive's own error with
|
|
150
|
+
that directive's own `Use:` hint. To show a directive name as text, escape it
|
|
151
|
+
with a backslash: `\:fetch`.
|
|
152
|
+
- **`WD251`**, `:state b = a` where `a` is declared state. It used to seed the
|
|
153
|
+
literal string `"a"` and track nothing. Derive it with `:computed` instead.
|
|
154
|
+
|
|
155
|
+
There are thirteen new codes in all, each with a cause, a fix and a compilable
|
|
156
|
+
example in `darkmown catalog --llms-full`.
|
|
157
|
+
|
|
158
|
+
## The rest
|
|
159
|
+
|
|
160
|
+
`:::` containers and `:button` now take `role`, `aria-*` and `title`, which was
|
|
161
|
+
the one place the no-attribute-syntax rule genuinely blocked a correct page.
|
|
162
|
+
A `:state` declared inside a closed `:if` branch is finally hydrated when the
|
|
163
|
+
branch opens. A static `@loop` fills a Markdown table. Renders settle, so a row
|
|
164
|
+
cloned mid-render gets its own `:if` state in the same pass. Rows keep their
|
|
165
|
+
place, so a re-render no longer steals focus from an input.
|
|
166
|
+
|
|
167
|
+
The [changelog](https://github.com/ZVN-DEV/darkmown/blob/master/CHANGELOG.md)
|
|
168
|
+
has all of it, and six new demo pages have it running:
|
|
169
|
+
[reactive links](/attr-binding/), [URL state](/url-state/),
|
|
170
|
+
[bound controls](/settings/), [server errors](/api-errors/),
|
|
171
|
+
[file upload](/upload/), and [closed-branch state](/branch-state/).
|
|
172
|
+
|
|
173
|
+
Upgrade with `npm install -D @zvndev/darkmown@latest`. Read the behavior
|
|
174
|
+
changes in the changelog first: interpolation now resolves in positions where
|
|
175
|
+
it used to stay literal, so a page that showed braces in an `href` will start
|
|
176
|
+
showing values.
|
|
@@ -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
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Reactive pages with no unsafe-eval
|
|
3
|
+
description: Since 2.1.0 the runtime interprets a validated expression AST instead of building a function, so reactive and static pages ship the same strict Content-Security-Policy.
|
|
4
|
+
date: 2026-07-11
|
|
5
|
+
excerpt: A reactive page used to force 'unsafe-eval' into your CSP. It no longer does, because the runtime stopped compiling expressions to JavaScript and started interpreting them.
|
|
6
|
+
schema: BlogPosting
|
|
7
|
+
tags: [security, runtime]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Reactive pages with no unsafe-eval
|
|
11
|
+
|
|
12
|
+
Every reactive framework has to answer one question honestly: how does the
|
|
13
|
+
browser evaluate `p.price < 50`?
|
|
14
|
+
|
|
15
|
+
Through Darkmown 2.0.x the answer was `new Function`. A `:computed`, a
|
|
16
|
+
`@loop … where`, a `.class when`, and a `:if` comparison were all validated at
|
|
17
|
+
compile time against a closed whitelist, then emitted as a small JavaScript
|
|
18
|
+
fragment that the runtime turned back into a callable. That is safe from the
|
|
19
|
+
injection angle, because nothing an author writes reaches the fragment unless it
|
|
20
|
+
matched the grammar first. It is not safe from the *deployment* angle, because
|
|
21
|
+
`new Function` needs `script-src 'unsafe-eval'`, and a security-minded adopter
|
|
22
|
+
who reads your CSP does not care how carefully you validated the input. They
|
|
23
|
+
care that the policy has a hole in it.
|
|
24
|
+
|
|
25
|
+
## What changed
|
|
26
|
+
|
|
27
|
+
The compiler still validates the expression exactly as before. What it emits is
|
|
28
|
+
different: instead of a JavaScript fragment, it serializes the already-validated
|
|
29
|
+
expression into a compact AST and puts it in the `data-wd-*` attribute. The
|
|
30
|
+
runtime walks that AST with a closed evaluator. Readers, unary operators, and a
|
|
31
|
+
fixed arithmetic, comparison, and logical set. An unknown op tag is a hard
|
|
32
|
+
error, never a fallthrough.
|
|
33
|
+
|
|
34
|
+
No `new Function`. No `eval`. Nothing on the page turns text into code.
|
|
35
|
+
|
|
36
|
+
## What it buys
|
|
37
|
+
|
|
38
|
+
The shipped policy is now byte-identical for static and reactive pages, across
|
|
39
|
+
`dist/_headers`, `vercel.json`, and the local `serve` layer:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
script-src 'self'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
No `'unsafe-inline'`, no `'unsafe-eval'`, on any page the framework emits. The
|
|
46
|
+
one inline script Darkmown ships (the `transitions: true` speculation-rules
|
|
47
|
+
block) is authorized by a build-time `'sha256-…'` hash, and the inline state
|
|
48
|
+
seed is a JSON data block that CSP does not gate at all.
|
|
49
|
+
|
|
50
|
+
## What it cost
|
|
51
|
+
|
|
52
|
+
136 bytes. The runtime went from 7,518 to 7,654 bytes gzipped, still under the
|
|
53
|
+
8 KB budget that CI enforces on every commit.
|
|
54
|
+
|
|
55
|
+
Semantics did not move. The interpreter applies the same JavaScript operators
|
|
56
|
+
the generated fragments used to, and a parity battery compares the two
|
|
57
|
+
evaluators across a wide matrix of expressions, states, and loop items, asserting
|
|
58
|
+
identical results. The fuzz suite still proves that hostile text survives only as
|
|
59
|
+
inert literal data.
|
|
60
|
+
|
|
61
|
+
## The rule underneath
|
|
62
|
+
|
|
63
|
+
Compile-time validation is the security layer, and it always was. Dropping
|
|
64
|
+
`eval` did not make the language safer against a malicious author, because the
|
|
65
|
+
whitelist already did that. It made the language **deployable** somewhere a
|
|
66
|
+
strict CSP is not negotiable, which is most places worth deploying.
|
|
@@ -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
|
|
@@ -16,7 +16,7 @@ none of them pay for it.
|
|
|
16
16
|
|
|
17
17
|
That is the whole bargain. A `.md` file is plain CommonMark. Rename it to `.wd`
|
|
18
18
|
and you unlock directives — state, loops, conditionals — but you only ship the
|
|
19
|
-
~
|
|
19
|
+
~6.6 KB runtime on the pages that actually use them. A listing built from a
|
|
20
20
|
collection loop resolves entirely at build time, so it stays static HTML.
|
|
21
21
|
|
|
22
22
|
## The extension is the feature gate
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
html: true
|
|
3
|
+
title: State inside a closed branch
|
|
4
|
+
description: A :state, :store or :theme declared inside an :if branch that starts closed is now hydrated the moment the branch opens, instead of starting undefined and never persisting.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
@include /nav.wd
|
|
8
|
+
@include /base.wd
|
|
9
|
+
|
|
10
|
+
<main>
|
|
11
|
+
|
|
12
|
+
<section class="intro">
|
|
13
|
+
<span class="eyebrow">Darkmown 2.7</span>
|
|
14
|
+
|
|
15
|
+
# State inside a closed branch
|
|
16
|
+
|
|
17
|
+
A closed `:if` branch lives in a `<template>`, and `querySelectorAll` does not look inside one. Anything declared in there was therefore never hydrated: it started `undefined`, ignored its `persist` token, and stayed that way for the life of the page. Now the runtime re-claims seed scripts whenever a render changes structure, so a branch that opens brings its declarations with it.
|
|
18
|
+
|
|
19
|
+
</section>
|
|
20
|
+
|
|
21
|
+
<section>
|
|
22
|
+
|
|
23
|
+
## Open the panel
|
|
24
|
+
|
|
25
|
+
:state panel = false
|
|
26
|
+
|
|
27
|
+
:button "Toggle the advanced panel" -> panel toggle
|
|
28
|
+
|
|
29
|
+
:if panel
|
|
30
|
+
|
|
31
|
+
::: section .panel
|
|
32
|
+
|
|
33
|
+
:state retries = 3 persist
|
|
34
|
+
|
|
35
|
+
The `:state retries` line above lives **inside** this branch. Before this change it was never declared at all: the counter below read empty and nothing was ever written to storage.
|
|
36
|
+
|
|
37
|
+
Retries: **{ retries }**
|
|
38
|
+
|
|
39
|
+
:button "More" -> retries++
|
|
40
|
+
:button "Fewer" -> retries--
|
|
41
|
+
|
|
42
|
+
Change it, close the panel, reopen it: the value you set is still there, because re-opening a branch does not re-seed a key state already holds. Reload the page and it is still there too, because `persist` now works from in here.
|
|
43
|
+
|
|
44
|
+
:::
|
|
45
|
+
|
|
46
|
+
:else
|
|
47
|
+
|
|
48
|
+
The panel is closed. Everything inside it, including its `:state`, is still in a `<template>`.
|
|
49
|
+
|
|
50
|
+
:endif
|
|
51
|
+
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
<section>
|
|
55
|
+
|
|
56
|
+
## The rules
|
|
57
|
+
|
|
58
|
+
- A seed is claimed once **per key**, not per node. Re-opening a branch clones fresh `<script>` nodes, so a per-node claim would have reset the reader's value every time.
|
|
59
|
+
- The seed recorded when the key is claimed is also its `reset` baseline, so a late declaration is resettable like any other.
|
|
60
|
+
- `persist` and `:store` read through storage at claim time, so a durable value declared in a branch comes back the first time the branch opens.
|
|
61
|
+
|
|
62
|
+
<a class="btn primary" href="/docs/#reactive-directives">Read the docs on reactive directives</a>
|
|
63
|
+
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
</main>
|
|
67
|
+
|
|
68
|
+
@include /footer.wd
|