@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.
Files changed (73) hide show
  1. package/AGENTS.md +392 -77
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +316 -49
  4. package/docs/cli.md +51 -17
  5. package/docs/spec-alignment.md +25 -4
  6. package/grammar/wd-directives.gbnf +13 -5
  7. package/package.json +7 -3
  8. package/site/_/base.skin +1 -1
  9. package/site/_/footer.wd +1 -1
  10. package/site/pages/api-errors/index.wd +81 -0
  11. package/site/pages/app.wd +1 -1
  12. package/site/pages/attr-binding/index.wd +84 -0
  13. package/site/pages/blog/darkmown-2-7-0.md +176 -0
  14. package/site/pages/blog/darkmown-2-7-0.skin +127 -0
  15. package/site/pages/blog/no-unsafe-eval.md +66 -0
  16. package/site/pages/blog/no-unsafe-eval.skin +127 -0
  17. package/site/pages/blog/zero-js-by-default.md +1 -1
  18. package/site/pages/branch-state/index.wd +68 -0
  19. package/site/pages/docs/index.wd +320 -19
  20. package/site/pages/index.wd +3 -1
  21. package/site/pages/playground/index.js +63 -56
  22. package/site/pages/pulse/index.wd +1 -1
  23. package/site/pages/settings/index.wd +97 -0
  24. package/site/pages/showcase/index.wd +37 -1
  25. package/site/pages/upload/index.wd +63 -0
  26. package/site/pages/url-state/index.wd +70 -0
  27. package/src/builder.js +222 -34
  28. package/src/catalog.js +217 -21
  29. package/src/cli.js +6 -3
  30. package/src/compiler/actions.js +80 -12
  31. package/src/compiler/body.js +104 -28
  32. package/src/compiler/collections.js +29 -13
  33. package/src/compiler/context.js +9 -2
  34. package/src/compiler/expr-ast.js +14 -0
  35. package/src/compiler/fetch.js +8 -4
  36. package/src/compiler/forms.js +133 -14
  37. package/src/compiler/frontmatter.js +28 -7
  38. package/src/compiler/includes.js +46 -6
  39. package/src/compiler/interpolation.js +56 -1
  40. package/src/compiler/loops.js +489 -35
  41. package/src/compiler/markdown.js +457 -44
  42. package/src/compiler/page.js +8 -3
  43. package/src/compiler/predicates.js +84 -14
  44. package/src/compiler/state.js +167 -18
  45. package/src/compiler/structure.js +159 -29
  46. package/src/errors.js +111 -3
  47. package/src/feeds.js +140 -7
  48. package/src/grammar.js +53 -10
  49. package/src/runtime.js +509 -130
  50. package/src/runtime.min.js +2 -0
  51. package/src/runtime.min.js.map +1 -0
  52. package/src/statics.js +46 -2
  53. package/src/templates/blog/README.md +2 -0
  54. package/src/templates/dashboard/README.md +2 -0
  55. package/src/templates/landing/README.md +2 -0
  56. package/src/templates/starter/README.md +1 -1
  57. package/src/templates/store/README.md +2 -0
  58. package/src/tools/validate.js +14 -4
  59. package/types/builder.d.ts +6 -2
  60. package/types/catalog.d.ts +12 -0
  61. package/types/compiler/actions.d.ts +1 -1
  62. package/types/compiler/body.d.ts +0 -3
  63. package/types/compiler/forms.d.ts +12 -1
  64. package/types/compiler/frontmatter.d.ts +8 -1
  65. package/types/compiler/includes.d.ts +16 -2
  66. package/types/compiler/interpolation.d.ts +23 -0
  67. package/types/compiler/loops.d.ts +47 -0
  68. package/types/compiler/markdown.d.ts +63 -1
  69. package/types/compiler/predicates.d.ts +7 -1
  70. package/types/compiler/state.d.ts +8 -2
  71. package/types/compiler/structure.d.ts +32 -1
  72. package/types/feeds.d.ts +68 -2
  73. package/types/statics.d.ts +25 -0
@@ -88,7 +88,7 @@ Sampled UTC, this morning
88
88
  **Source**
89
89
 
90
90
  ::: div .stat-value
91
- :fetch
91
+ \:fetch
92
92
  :::
93
93
 
94
94
  Live JSON endpoint
@@ -0,0 +1,97 @@
1
+ ---
2
+ html: true
3
+ title: Bound controls: a select, a radio group and a checkbox
4
+ description: Outside a :form, the three field directives bind straight to a declared :state, so a settings panel drives :if regions with no form and no JavaScript of your own.
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
+ # Bound controls
16
+
17
+ `:select`, `:radio` and `:checkbox` mean two things, and where they sit decides which. Inside a `:form` they are form fields, submitted by name. Outside one they bind to a declared `:state` the way `:bind` and `:slider` already do: move the control and the state changes, change the state and the control moves.
18
+
19
+ </section>
20
+
21
+ <section>
22
+
23
+ ## A settings panel
24
+
25
+ :state density = "Comfortable"
26
+ :state theme_pick = "System"
27
+ :state previews = true
28
+
29
+ **Density**
30
+
31
+ :select density
32
+ - Compact
33
+ - Comfortable
34
+ - Spacious
35
+
36
+ **Colour**
37
+
38
+ :radio theme_pick
39
+ - System
40
+ - Light
41
+ - Dark
42
+
43
+ **Previews**
44
+
45
+ :checkbox previews
46
+ - Show image previews
47
+
48
+ </section>
49
+
50
+ <section>
51
+
52
+ ## What the panel drives
53
+
54
+ Every region below reads the same state the controls write.
55
+
56
+ :if density == "Compact"
57
+ Compact: rows sit tight together.
58
+ :else if density == "Spacious"
59
+ Spacious: rows get room to breathe.
60
+ :else
61
+ Comfortable: the default spacing.
62
+ :endif
63
+
64
+ :if theme_pick == "System"
65
+ Following the operating system's colour scheme.
66
+ :else
67
+ Forcing **{ theme_pick }** regardless of the system setting.
68
+ :endif
69
+
70
+ :if previews
71
+ Image previews are on.
72
+ :else
73
+ Image previews are off, so the list loads text only.
74
+ :endif
75
+
76
+ Settings as one line each: **{ density }**, **{ theme_pick }**, previews { previews }.
77
+
78
+ :button "Reset to defaults" -> density reset; theme_pick reset; previews reset
79
+
80
+ </section>
81
+
82
+ <section>
83
+
84
+ ## The rules
85
+
86
+ - The state has to exist first. A bound field with no matching `:state` is a compile error that names both readings, so a typo can never become a silently inert control.
87
+ - A bound `:radio` group carries the chosen option's text; the group keeps a shared `name` so the browser makes it mutually exclusive.
88
+ - A bound `:checkbox` carries a **boolean**, so it takes exactly one `- Label` line, the label to show beside it. The multi-value checkbox group is the in-form behavior and is unchanged.
89
+ - Inside a `:form` nothing changed at all, even if a `:state` of the same name happens to exist elsewhere on the page.
90
+
91
+ <a class="btn primary" href="/docs/#bound-controls-outside-a-form">Read the docs on bound controls</a>
92
+
93
+ </section>
94
+
95
+ </main>
96
+
97
+ @include /footer.wd
@@ -96,7 +96,7 @@ Darkmown's whole pitch is weight. A static page ships **zero** framework JavaScr
96
96
 
97
97
  ::: card .sc-scale
98
98
  - **Static Darkmown page** — `0 KB` framework JS. Forever.
99
- - **Reactive Darkmown page** — one shared `~7.7 KB` gzipped runtime, no per-component bundles.
99
+ - **Reactive Darkmown page** — one shared `~6.6 KB` gzipped runtime, no per-component bundles.
100
100
  - **A typical SPA** — ships a framework runtime in the **~15–60 KB** gzipped range *before* your app code, on every page (Alpine ~15 KB, Vue ~18 KB runtime, React 19 ~59 KB).
101
101
  :::
102
102
 
@@ -160,6 +160,42 @@ Prefer to see each feature in isolation? These pages tour the building blocks th
160
160
  [Open →](/markdown/)
161
161
  :::
162
162
 
163
+ ::: card .sc-mini
164
+ **Reactive links**: a destination that follows state.
165
+
166
+ [Open →](/attr-binding/)
167
+ :::
168
+
169
+ ::: card .sc-mini
170
+ **URL state**: `from-url`, a filter you can share.
171
+
172
+ [Open →](/url-state/)
173
+ :::
174
+
175
+ ::: card .sc-mini
176
+ **Bound controls**: select, radio, checkbox, no form.
177
+
178
+ [Open →](/settings/)
179
+ :::
180
+
181
+ ::: card .sc-mini
182
+ **Server errors**: what the API actually said.
183
+
184
+ [Open →](/api-errors/)
185
+ :::
186
+
187
+ ::: card .sc-mini
188
+ **File upload**: a `:form` that posts multipart.
189
+
190
+ [Open →](/upload/)
191
+ :::
192
+
193
+ ::: card .sc-mini
194
+ **Branch state**: a `:state` inside a closed `:if`.
195
+
196
+ [Open →](/branch-state/)
197
+ :::
198
+
163
199
  </div>
164
200
 
165
201
  </section>
@@ -0,0 +1,63 @@
1
+ ---
2
+ html: true
3
+ title: File upload: a :form that posts multipart
4
+ description: A :form containing a file field submits as multipart FormData instead of a urlencoded body, so the file itself travels. The failure path renders the server's own message and its field-level errors.
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
+ # File upload
16
+
17
+ A urlencoded body can only carry a file's **name**. A `:form` that contains a file field now declares `enctype="multipart/form-data"` and the runtime posts the `FormData` itself, letting the browser write the multipart boundary. Nothing else about the form changes.
18
+
19
+ </section>
20
+
21
+ <section>
22
+
23
+ ## Pick a file and send it
24
+
25
+ :form into reply action="/api/upload/"
26
+ :input photo type=file required
27
+ :input caption placeholder="A caption (optional)"
28
+ :submit "Upload"
29
+ :endform
30
+
31
+ :if reply
32
+ Uploaded **{ reply.name }**, { reply.size } bytes, type `{ reply.type }`.
33
+ :if reply.caption
34
+ Caption: { reply.caption }
35
+ :endif
36
+ :endif
37
+
38
+ :if reply_error
39
+ **{ reply_error }**
40
+ :if reply_error_body
41
+ The server also said which field was wrong: { reply_error_body.fields.photo }
42
+ :endif
43
+ :endif
44
+
45
+ Submit with no file to see the failure path: the endpoint answers `422` with a JSON body, and the page renders the server's own sentence rather than "HTTP 422".
46
+
47
+ </section>
48
+
49
+ <section>
50
+
51
+ ## The rules
52
+
53
+ - A file field makes the whole form multipart. The compiler writes the `enctype` for the browser's native submit, and the runtime sends the `FormData` with **no** content-type of its own so the boundary is correct.
54
+ - Bound controls inside the form (a `:bind`, a bound `:select`) carry no `name`, so `FormData` never sees them. On the multipart path they are appended by their state key rather than silently dropped.
55
+ - A file field with `method="get"` is a compile error. A GET request has no body, so only the file's name would ever travel, the kind of failure that looks like it worked.
56
+
57
+ <a class="btn primary" href="/docs/#file-upload">Read the docs on file upload</a>
58
+
59
+ </section>
60
+
61
+ </main>
62
+
63
+ @include /footer.wd
@@ -0,0 +1,70 @@
1
+ ---
2
+ html: true
3
+ title: The URL is state: :state … from-url
4
+ description: A filter that survives a reload and travels in a shared link. from-url mirrors a :state into the query string with replaceState, drops the parameter when the value is back at its default, and re-reads on back and forward.
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
+ # The URL is state
16
+
17
+ A filter nobody can link to is half a feature. Add `from-url` to a `:state` and the value lives in the query string as well as in memory: reload keeps it, a shared link arrives with it applied, and back and forward move through it.
18
+
19
+ </section>
20
+
21
+ <section>
22
+
23
+ ## Search and filter
24
+
25
+ :state q = "" from-url
26
+ :state tier = "all" from-url
27
+
28
+ :bind q placeholder="Search products"
29
+
30
+ :radio tier
31
+ - all
32
+ - budget
33
+ - premium
34
+
35
+ :state products = [
36
+ { "name": "Aurora Lamp", "price": 49, "tier": "budget" },
37
+ { "name": "Briza Fan", "price": 89, "tier": "budget" },
38
+ { "name": "Cove Shelf", "price": 240, "tier": "premium" },
39
+ { "name": "Dune Rug", "price": 310, "tier": "premium" }
40
+ ]
41
+
42
+ Tier **{ tier }**, search "{ q }".
43
+
44
+ @loop products into p where p.name contains q
45
+ - **{ p.name }** · { p.price | money } ({ p.tier })
46
+ @empty
47
+ Nothing matches that search.
48
+ @endloop
49
+
50
+ Type in the box and watch the address bar. Then reload the page: the search and the tier come back. Copy the URL into a new tab and it opens on the same view. The tier radio is a second key riding along in the same URL; it is shown above the list rather than wired into the filter, so you can see two `from-url` keys travel together.
51
+
52
+ </section>
53
+
54
+ <section>
55
+
56
+ ## The rules
57
+
58
+ - The parameter is named after the state key. A section-scoped key like `cart:items` becomes `cart.items`, so the name is always readable and always unique.
59
+ - A value equal to its declared seed **drops** its parameter, so the default page keeps a clean URL.
60
+ - Writes go through `history.replaceState`, so filtering never fills the back button with one entry per keystroke.
61
+ - Combine it with `persist` and the precedence is URL first, then the stored value, then the seed, so a link you were sent wins over what this browser remembers.
62
+ - `from-url` belongs to `:state` only. A `:store` is shared by every page and every tab, while a query parameter belongs to one page's address, so putting `from-url` on a `:store` is a compile error rather than a guess.
63
+
64
+ <a class="btn primary" href="/docs/#url-as-state-from-url">Read the docs on URL as state</a>
65
+
66
+ </section>
67
+
68
+ </main>
69
+
70
+ @include /footer.wd