@zvndev/darkmown 0.4.0 → 0.6.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 ADDED
@@ -0,0 +1,297 @@
1
+ # Building with Darkmown — agent guide
2
+
3
+ Darkmown is a Markdown-native web framework. You build sites by writing `.md` and `.wd` files. This sheet is everything you need. Read it fully before writing files, and **always ship styling** — an unstyled page is a failed page.
4
+
5
+ ## The one rule
6
+
7
+ - `.md` files are plain CommonMark. Directives stay inert text.
8
+ - `.wd` files are the same Markdown **plus directives** (state, loops, conditionals, includes, forms).
9
+ - To make a `.md` page interactive, **rename it to `.wd`**. ⚠️ Renaming means the old `.md` is **gone** — never leave both `index.md` and `index.wd`, that is a fatal `Duplicate route` build error. Upgrade = delete the `.md`, create the `.wd`.
10
+ - Static pages ship **zero** JavaScript; a page loads the ~2 KB runtime only if it declares reactive behavior.
11
+
12
+ ## Project layout & routing
13
+
14
+ ```
15
+ site/
16
+ pages/ ← the route tree. index.wd → /, about.wd → /about/, docs/index.wd → /docs/
17
+ _/ ← include shelf + shared partials (not routed). nav.wd and shelf JSON live here.
18
+ package.json
19
+ ```
20
+ - **Folders are routes.** `site/pages/blog/post.wd` serves at `/blog/post/`.
21
+ - Files/folders starting with `.`, `-`, or `_` are **hidden** from routing (drafts, partials).
22
+ - A `page.skin` next to `page.wd` attaches styling; a `page.js` attaches behavior. Matched by basename.
23
+
24
+ Commands: `darkmown dev`, `darkmown build`, `darkmown serve`.
25
+
26
+ ## Interpolation — one syntax
27
+
28
+ `{ name }` or `{ name.path }`. Resolves in order: loop item → in-scope value → declared `:state` → otherwise literal text. **Declare state before you bind it** — compilation is line-based, top to bottom, so a `{ x }` or `:if x` must come *after* the `:state x` (or the `:form into x`) that creates it.
29
+
30
+ ## Directives — the complete set (nothing else exists)
31
+
32
+ ### State and buttons
33
+ ```wd
34
+ :state count = 0
35
+ :state cart = [] persist ← persist = localStorage
36
+
37
+ Count: { count }
38
+
39
+ :button "Add" -> count++
40
+ :button "Add 5" -> count += 5
41
+ :button "Add item" -> cart += {"id": 1, "name": "Sticker"}
42
+ :button "Reset" -> count = 0
43
+ ```
44
+ Button actions are a **fixed whitelist**: `x++`, `x--`, `x += <number>`, `list += <json>`, `x = <json>`. Target must be declared `:state`. Values are JSON literals — they cannot reference loop items or other state. **`:button` is for state actions only** — for a link styled as a button, write raw HTML `<a class="btn" href="…">`.
45
+
46
+ ### Conditionals
47
+ ```wd
48
+ :if count
49
+ Shown when truthy.
50
+ :else
51
+ Shown when falsy.
52
+ :endif
53
+ ```
54
+ `:if name.path` tests a dotted path. Conditionals nest, including per-row inside a loop.
55
+
56
+ ### Loops — the only loop
57
+ ```wd
58
+ @loop /products.json into product ← JSON file: unrolled at build time
59
+ - **{ product.name }** — { product.price }
60
+ @endloop
61
+
62
+ :state todos = [{"id": 1, "title": "Ship"}]
63
+ @loop todos into todo ← :state list: reactive, patched by key
64
+ - { todo.title }
65
+ @endloop
66
+ ```
67
+ Source: a JSON file path, an in-scope value, or a `:state` list. Loops nest; `:if todo.done` branches per row.
68
+
69
+ ### Filtering — `@loop … where` + live search with `:bind`
70
+ ```wd
71
+ @loop /products.json into p where p.featured == true and p.price < 80
72
+ - { p.name } ← item-only predicate → filtered at BUILD time, stays zero-JS
73
+ @endloop
74
+
75
+ :state products = [{"id":1,"name":"Aurora"},{"id":2,"name":"Briza"}]
76
+ :state q = ""
77
+ :bind q placeholder="Search" ← <input> bound two-way to :state q
78
+ @loop products into p where p.name contains q ← predicate reads :state → reactive, re-filters live
79
+ - { p.name }
80
+ @endloop
81
+ ```
82
+ `where` joins conditions with `and` / `or`. Operators: `==` `!=` `<` `<=` `>` `>=` and `contains` (case-insensitive substring). Operands are item paths (`p.field`), declared `:state`, numbers, or `"strings"` — whitelist-validated, no expressions. **Rule that decides JS: if the predicate only reads the row, it filters at build time and the page stays static; if it reads any `:state`, the loop becomes reactive.** `:bind name` is the input primitive for live filtering — it accepts `type=` (default `text`), `placeholder=`, `autocomplete=`, and `required` / `autofocus`.
83
+
84
+ ### Includes & sections
85
+ ```wd
86
+ @include /nav.wd ← partial from site/_/
87
+ @include /card.wd with title={ row.name }
88
+
89
+ ::: section #cart .panel ← container with id/classes; state inside is scoped to it
90
+ :state items = []
91
+ { items.length } items
92
+ :::
93
+ ```
94
+
95
+ ### Data, forms, computed
96
+ ```wd
97
+ :fetch team from "/__wd/data/team.json" ← loads JSON into state; sets team_error on failure
98
+ :fetch quotes from "/q.json" when=visible ← lazy, on scroll into view
99
+
100
+ :form into profile ← captures submit into state (declares `profile`), no backend
101
+ :input name placeholder="Your name" required
102
+ :input email type=email placeholder="Email" ← set type= for email/number/etc (default is text)
103
+ :submit "Save"
104
+ :endform
105
+
106
+ :if profile ← read form state AFTER :endform (see rule below)
107
+ Thanks, **{ profile.name }**!
108
+ :endif
109
+
110
+ :computed total = items.length * 4 ← derived state; arithmetic & comparisons only
111
+ ```
112
+ Shelf `.json` files (in `site/_/`) publish to `/__wd/data/`. `:form action="/api/x" into reply` posts urlencoded and lands the JSON reply in state (without JS it degrades to a native POST). Darkmown owns no backend — point forms at your own API.
113
+
114
+ **Form-state rules (these cause most form failures):**
115
+ - `:form into x` declares `x` only at `:endform`. A `:if x` (or `{ x.field }`) that reads it must come **after `:endform`**, never inside the form body.
116
+ - State declared inside a `::: section` is **scoped to that section** (as `sectionId:x`). If you wrap a `:form into x` in a section, a `:if x` *outside* that section can't see it. Keep the form and the `:if` that reads it in the **same scope** — or don't wrap the form in a section.
117
+
118
+ ## What HTML each directive emits — target these in `.skin`
119
+
120
+ Your `.skin` selectors must match the **real** output. The emitted HTML is:
121
+
122
+ | You write | It renders as |
123
+ |---|---|
124
+ | `# Heading`, prose, `- list` | normal `<h1>`, `<p>`, `<ul><li>` (style by tag) |
125
+ | `::: section #id .card … :::` | `<section id="id" class="card">…</section>` |
126
+ | `::: name .card … :::` (non-`section`) | `<div class="card">…</div>` |
127
+ | `:button "x" -> …` | `<button>x</button>` |
128
+ | `:input email …` | `<input type="…">` |
129
+ | `:bind q placeholder="…"` | `<input>` bound two-way to `:state q` |
130
+ | `:submit "Go"` | `<button type="submit">Go</button>` |
131
+ | `:form …` | `<form>…</form>` |
132
+ | reactive `@loop` of a list | `<ul>`/`<ol>` (or `<div>`) of items |
133
+ | `{ name }` | a text node |
134
+
135
+ So style with real selectors: `section`, `.card`, `button`, `input`, `form`, `h1`. **To add a class to a container, use `::: name .class`. For any other custom element, write raw HTML `<div class="…">`. There is NO `{.class}` attribute syntax.**
136
+
137
+ ## Styling with `.skin` — ship this on every page
138
+
139
+ A colocated `.skin` compiles to CSS. It is indentation-structural: a line is a **selector** if the next line is indented under it; otherwise it is a `property value` declaration. Start from a real design system — tokens, a type scale, spacing, a responsive grid:
140
+
141
+ ```skin
142
+ tokens ← becomes :root CSS variables ($name → var(--name))
143
+ ink #14181f
144
+ muted #5b6470
145
+ paper #ffffff
146
+ bg #f6f7f9
147
+ accent #4f46e5
148
+ line #e6e8ec
149
+ radius 14px
150
+
151
+ body
152
+ margin 0
153
+ font 16px/1.6 ui-sans-serif, system-ui, -apple-system, sans-serif
154
+ color $ink
155
+ background $bg
156
+
157
+ main
158
+ max-width 960px
159
+ margin 0 auto
160
+ padding 4rem 1.5rem
161
+
162
+ h1
163
+ font-size 3rem
164
+ line-height 1.05
165
+ letter-spacing -0.03em
166
+ margin 0 0 1rem
167
+
168
+ .hero
169
+ padding 5rem 2rem
170
+ border-radius $radius
171
+ background linear-gradient(135deg, #4f46e5, #9333ea)
172
+ color white
173
+
174
+ .grid
175
+ display grid
176
+ grid-template-columns repeat(3, 1fr)
177
+ gap 1.25rem
178
+
179
+ .card
180
+ background $paper
181
+ border 1px solid $line
182
+ border-radius $radius
183
+ padding 1.5rem
184
+
185
+ .actions
186
+ display flex
187
+ gap .75rem
188
+ flex-wrap wrap
189
+
190
+ button
191
+ font-size 1rem
192
+ padding .7rem 1.2rem
193
+ border 0
194
+ border-radius 10px
195
+ background $accent
196
+ color white
197
+ cursor pointer
198
+ transition transform .15s ease, box-shadow .15s ease
199
+
200
+ button:hover
201
+ transform translateY(-1px)
202
+ box-shadow 0 6px 20px rgba(79,70,229,.3)
203
+
204
+ input
205
+ width 100%
206
+ padding .7rem .9rem
207
+ border 1px solid $line
208
+ border-radius 10px
209
+ font-size 1rem
210
+
211
+ input:focus-visible
212
+ outline 2px solid $accent
213
+ outline-offset 2px
214
+
215
+ @media (max-width: 640px)
216
+ .grid
217
+ grid-template-columns 1fr
218
+ h1
219
+ font-size 2.1rem
220
+ ```
221
+ Always include: a tokens block, a real type scale, generous spacing, a `max-width` centered `main`, styled `button`/`input` with `:hover`/`:focus-visible` states, and a responsive `@media` rule (`.skin` supports `@media` — its indented rules are wrapped automatically). The `font` property is the CSS shorthand when it leads with a size (`font 16px/1.6 system-ui`) and `font-family` otherwise. Prefer `.skin`; a plain `<style>` block in a `.wd` also works.
222
+
223
+ **Layout gotcha — CTAs and link rows.** Markdown wraps adjacent links/buttons on consecutive lines into a single `<p>`, so putting `display:flex` on their container won't space them. To lay out a row of buttons or nav links, wrap them in explicit raw HTML and style that:
224
+ ```wd
225
+ <div class="actions">
226
+ <a class="btn" href="/signup">Start free</a>
227
+ <a class="btn ghost" href="/docs">Docs</a>
228
+ </div>
229
+ ```
230
+
231
+ ### Page structure & hierarchy — required for styles to apply
232
+ - **Wrap every page's body in `<main>`.** Your `main { … }` rules (centering, `max-width`, page padding) are dead unless the element exists.
233
+ - **One `#` per page** (the H1/title). Use `##` for section headings (they render as `<h2>` — style those). Never emit two `<h1>`s.
234
+ - **Only style classes you actually attach.** Add a class with `::: name .card` (container) or raw HTML `class="…"`. Don't write `.thanks { … }` then forget to put `.thanks` on the element — the styling silently does nothing. (Wrap a styled confirmation in `::: section .thanks … :::`.)
235
+ - **Make cards feel real:** a subtle `box-shadow`, a `:hover` lift, and a distinct price/secondary-text color or weight. Avoid emoji as icons — they read as a default-AI tell; prefer an inline SVG or a styled monogram.
236
+ - **Markdown is NOT parsed inside raw HTML blocks.** `**bold**` or `_em_` written inside a `<div>…</div>` renders as literal asterisks. Inside raw HTML use real tags (`<strong>`, `<em>`); use Markdown `**` only in plain prose lines.
237
+
238
+ ## The escape hatch — for logic directives can't express
239
+
240
+ Directives are narrow on purpose. For real logic (filtering, custom interactions), use a colocated `.js` with `window.wd` (`wd.get`, `wd.set`, `wd.state`, `wd.render`). Section state is keyed `sectionId:name`.
241
+
242
+ **Canonical "filter a list" pattern** (filtering is not a directive). Keep the fetched/source list intact and filter the rendered DOM — do not overwrite the source state, do not poll with setInterval:
243
+ ```wd
244
+ <input id="q" placeholder="Search" autocomplete="off">
245
+
246
+ :fetch products from "/__wd/data/products.json"
247
+ :if products
248
+ @loop products into p
249
+ ::: section .card
250
+ **{ p.name }** — { p.price }
251
+ :::
252
+ @endloop
253
+ :endif
254
+ ```
255
+ ```js
256
+ // products.js
257
+ const q = document.getElementById("q");
258
+ q.addEventListener("input", () => {
259
+ const term = q.value.toLowerCase();
260
+ for (const card of document.querySelectorAll(".card")) {
261
+ card.style.display = card.textContent.toLowerCase().includes(term) ? "" : "none";
262
+ }
263
+ });
264
+ ```
265
+ This visual DOM filter is fine for a static fetched list. (Note: a `wd.render()` triggered by other state changes can reset the toggled `display`; for a list that also mutates reactively, re-apply the filter after such changes.)
266
+
267
+ ## Hard rules — do not break these
268
+
269
+ 1. **Never invent directives or syntax.** These do NOT exist: `@section`/`@endsection`, `{% if %}`, `{.class}` attribute lists, `:for`, `v-if`, JSX. The only container is `::: … :::`; the only loop is `@loop`; the only interpolation is `{ }`.
270
+ 2. **Always ship styling.** A `.skin` (or `<style>`) with a modern type scale, spacing, and a responsive rule — every page. Unstyled = failure.
271
+ 3. **`.skin` must target emitted HTML** (see the table). Verify selectors match real elements.
272
+ 4. **Rename = delete.** Upgrading `.md`→`.wd` means removing the `.md`. Never leave both.
273
+ 5. **Declare before you bind.** `:state` (or `:form into x`) must appear before any `{ x }` / `:if x` that reads it.
274
+ 6. **Whitelisted grammars only.** Button/`:computed` actions are fixed forms; arbitrary logic goes in a `.js` escape hatch.
275
+
276
+ ## Minimal complete page
277
+
278
+ ```wd
279
+ ---
280
+ title: Hello
281
+ ---
282
+
283
+ @include /nav.wd
284
+
285
+ <main class="hero">
286
+
287
+ # Hello
288
+
289
+ :state count = 0
290
+
291
+ You clicked { count } times.
292
+
293
+ :button "Click" -> count++
294
+
295
+ </main>
296
+ ```
297
+ …with a colocated `index.skin` styling `.hero`, `button`, and the type scale.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
4
4
 
5
+ ## 0.6.0 — 2026-06-13
6
+
7
+ - **State-driven list filtering — `@loop … where <predicate>`.** Filter any loop with conditions that join via `and` / `or`. Operators: `==` `!=` `<` `<=` `>` `>=` and `contains` (case-insensitive substring). Operands are item paths, declared `:state`, numbers, or `"strings"` — a compile-time whitelist (no expressions, no `eval`; `constructor`/`prototype`/`__proto__` rejected in compiler and runtime). **The predicate decides reactivity:** an item-only predicate filters at build time and the page stays zero-JS; a predicate that reads `:state` compiles to a reactive filtered loop driven by the keyed reconciler (rows are baked in for static JSON sources). This replaces the fragile DOM-toggle escape hatch — the #1 gap surfaced by the agent-eval benchmark.
8
+ - **`:bind <state>`** — a two-way `<input>` bound to a `:state` value, the input primitive behind live search. Accepts `type=` (default `text`), `placeholder=`, `autocomplete=`, and the `required` / `autofocus` flags. The field reflects state changes back unless it is focused.
9
+ - **`.skin` robustness:** `/* … */` block comments and decorative divider lines (`----`, `* * *`) are skipped instead of mis-parsed as rules.
10
+ - Scaffold (`darkmown init`): ships a styled default nav (brand + links, hover states) and an `about.md` companion page so a fresh project looks finished and the nav links resolve.
11
+ - Live demo: the reactive page now includes a real "search in pure Markdown" section powered by `@loop … where` + `:bind`.
12
+
13
+ ## 0.5.0 — 2026-06-12
14
+
15
+ - **Nested `:if` over loop items.** Conditionals inside a reactive `@loop` now nest — an inner `:if` resolves after the outer branch, per row, both at build time (balanced-region pre-render) and at runtime (recursive fill). Previously the compiler threw "not supported yet"; the loop-template fill no longer relies on a non-greedy regex that broke on nesting.
16
+ - **VS Code extension** (`editors/vscode`): syntax highlighting, snippets, and folding for `.wd` and `.skin` files. Grammars are tested through the real VS Code tokenizer (vscode-textmate) in CI.
17
+ - New `/app/` demo — a whole live app in one `.wd` file: static build-time loop, fetched data looped with nested conditionals, a persistent counter with a computed milestone, and a form that round-trips to a real server. It is the framework's elevator pitch as a single page.
18
+ - Site: landing page repositioned around the core wedge ("Markdown that runs" — full reactive websites in plain Markdown). `description`/Open Graph/Twitter meta tags now emit from frontmatter. Runtime-size claims corrected to the true ~2 KB; mobile nav wraps instead of overflowing.
19
+
5
20
  ## 0.4.0 — 2026-06-11
6
21
 
7
22
  - **Renamed**: the framework is now **Darkmown** (formerly Markie). Package `@zvndev/darkmown` (npm's typosquat guard reserves bare `darkmown` as too similar to `markdown`), CLI `darkmown`. The `.wd` format and `/__wd/` asset paths are unchanged.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **[darkmown.com](https://darkmown.com)** · markdown, rearranged.
4
4
 
5
- Darkmown is a Markdown-native web framework. Two formats, one rule: `.md` stays plain CommonMark forever, and renaming a file to `.wd` ("whateverdown") is what unlocks directives — includes, loops, state, conditionals, and sections. Static pages ship **zero** framework JavaScript; reactive pages share one runtime under 2 KB gzipped.
5
+ Darkmown is a Markdown-native web framework. Two formats, one rule: `.md` stays plain CommonMark forever, and renaming a file to `.wd` ("whateverdown") is what unlocks directives — includes, loops, state, conditionals, and sections. Static pages ship **zero** framework JavaScript; reactive pages share one runtime around 2 KB gzipped (CI-enforced under 5 KB).
6
6
 
7
7
  ## Quick start
8
8
 
@@ -45,7 +45,7 @@ npm run dev # live demo site — the same site that runs darkmown.com
45
45
  - Files or folders starting with `.`, `-`, or `_` are hidden from routing.
46
46
  - `site/_` is the include shelf for `@include /name.wd`.
47
47
  - Matching `page.skin` and `page.js` colocate styling and behavior by basename.
48
- - Static pages ship zero Darkmown runtime. Reactive pages share `/__wd/runtime.js` (currently ~1.6 KB gzipped).
48
+ - Static pages ship zero Darkmown runtime. Reactive pages share `/__wd/runtime.js` (currently ~2 KB gzipped, CI-enforced under 5 KB).
49
49
  - Shelf `.json` files are published at `/__wd/data/` so `:fetch` works on any static host.
50
50
 
51
51
  ## Interpolation
@@ -74,6 +74,33 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
74
74
 
75
75
  Loops nest, dotted paths reach into rows, and `@include ... with x={ row.field }` reassigns values Liquid-style.
76
76
 
77
+ ### Filtering — `@loop … where`
78
+
79
+ Add `where <predicate>` to filter a loop. Conditions compare a loop-item field against a number, a string, or another value, and join with `and` / `or`:
80
+
81
+ ```wd
82
+ @loop /products.json into p where p.featured == true and p.price < 80
83
+ - { p.name }
84
+ @endloop
85
+ ```
86
+
87
+ Operators: `==` `!=` `<` `<=` `>` `>=`, plus `contains` for case-insensitive substring match. The predicate is a compile-time-validated whitelist — only item paths, declared `:state`, numbers, and `"strings"` are allowed (no arbitrary expressions, no `eval`).
88
+
89
+ **The source decides reactivity, just like the loop itself.** If the predicate only reads the row, the filter runs at build time and the page stays **zero-JS**. If the predicate reads a `:state` value, the loop becomes reactive and re-filters live as that state changes — a live search in pure Markdown:
90
+
91
+ ```wd
92
+ :state products = [{"id":1,"name":"Aurora Lamp"},{"id":2,"name":"Briza Fan"}]
93
+ :state q = ""
94
+
95
+ :bind q placeholder="Search"
96
+
97
+ @loop products into p where p.name contains q
98
+ - { p.name }
99
+ @endloop
100
+ ```
101
+
102
+ `:bind <state>` renders an `<input>` wired two-way to a `:state` value — typing updates the state, and the state reflects back into the field. It accepts `type=` (default `text`), `placeholder=`, `autocomplete=`, and the `required` / `autofocus` flags.
103
+
77
104
  ## Sections
78
105
 
79
106
  ```wd
@@ -132,12 +159,16 @@ Loading…
132
159
  - `:form into name` captures submits straight into state (no backend). `:form action="/url"` emits a plain native form instead — zero JS, full progressive enhancement.
133
160
  - `:form action="/url" into reply` does both: with JS the submit posts urlencoded via fetch and the JSON reply lands in state `reply` (`reply_error` on failure); without JS it is the same native POST. Darkmown adapts to any backend — it does not own one. `darkmown dev` ships a `/__wd/echo` endpoint for demos.
134
161
  - `:state x = [] persist` keeps that state in localStorage across reloads.
135
- - `:if item.path` works inside reactive loops for per-row branches.
162
+ - `:if item.path` works inside reactive loops for per-row branches, and nests — an inner `:if` resolves after the outer branch and stays reactive.
136
163
 
137
164
  ## The escape hatch
138
165
 
139
166
  Reactive pages expose `window.wd` — `wd.get(key)`, `wd.set(key, value)`, `wd.state`, `wd.render()` — so colocated `.js` can do anything the directives can't. Section-scoped keys are addressed as `sectionId:name`.
140
167
 
168
+ ## Editor support
169
+
170
+ A VS Code extension in [`editors/vscode`](editors/vscode) gives `.wd` and `.skin` files syntax highlighting, snippets, and folding — so a `.wd` file reads as Markdown-plus-directives, never as broken Markdown. Build it with `cd editors/vscode && npx @vscode/vsce package`, or install the published extension from the Marketplace (search "Darkmown").
171
+
141
172
  ## Spec status
142
173
 
143
174
  See [docs/spec-alignment.md](docs/spec-alignment.md) for the deep alignment audit against the original vision.
@@ -20,7 +20,7 @@ This audit compares the current Darkmown implementation with the original MARROW
20
20
  - **`:fetch name from "url"`:** declarative data loading into reactive state, with `name_error` for failures. Shelf `.json` publishes to `/__wd/data/` so it works on static hosts.
21
21
  - **`:form` two ways:** `:form into name` captures submits into client state with zero backend; `:form action="/url"` emits a plain native form and ships no JS — the progressive-enhancement story from the original doc.
22
22
  - **`:state x = v persist`:** localStorage-backed state. This is the honest client-side slice of the original `:cart` primitive; server sync remains future work.
23
- - **`:if item.path` inside reactive loops:** per-row conditional branches, filled at compile time and re-filled by the keyed patcher.
23
+ - **`:if item.path` inside reactive loops:** per-row conditional branches, filled at compile time and re-filled by the keyed patcher. Conditionals **nest** — an inner `:if` is resolved after the outer branch, both at build time (balanced-region pre-render) and at runtime (recursive fill).
24
24
  - **`window.wd` escape hatch:** `get`/`set`/`state`/`render` exposed to colocated `.js` — the adapted form of the original `<script scope>` "infinite extension" idea, aligned with Darkmown's colocation model instead of inline scripts.
25
25
 
26
26
  ## Deliberate adaptations
@@ -46,10 +46,19 @@ This audit compares the current Darkmown implementation with the original MARROW
46
46
  - **`darkmown serve`:** local preview of the built `dist`, no dev client injected.
47
47
  - **Packaging:** v0.3.0, license UNLICENSED pending a licensing decision, scaffold pins the live version, npm pack contents covered by tests.
48
48
 
49
+ ## Stage 6 additions (2026-06-13)
50
+
51
+ - **Nested `:if` over loop items (v0.5.0):** conditionals nest inside reactive `@loop`, per row. Both fill paths fixed — build-time balanced `matchElement` recursion and runtime `fillItem` recursion.
52
+ - **State-driven list filtering — `@loop … where <predicate>`:** the #1 need surfaced by the agent-eval benchmark. Conditions (`field <op> value`, plus `contains`) join with `and` / `or` over a compile-time-validated whitelist — item paths, declared `:state`, numbers, strings; `constructor`/`prototype`/`__proto__` rejected, no `eval`. The source decides reactivity, matching the loop itself: an item-only predicate filters at build time and stays zero-JS; a predicate that reads `:state` compiles to a reactive filtered loop (rows baked in for static sources). This replaces the fragile DOM-toggle escape hatch with the keyed reconciler.
53
+ - **`:bind <state>`:** two-way `<input>` bound to a `:state` value — the input primitive that powers live search. Accepts `type=`, `placeholder=`, `autocomplete=`, `required`, `autofocus`.
54
+ - **`.skin` robustness:** `@media`/`@supports` at-rules wrap nested rules; the `font` shorthand passes through (size-led or containing `/`) while `font <stack>` still aliases to `font-family`; `/* … */` block comments and decorative divider lines are skipped.
55
+ - **Editor tooling:** VS Code extension (`editors/vscode`) ships `.wd` + `.skin` TextMate grammars, snippets, and folding, grammar-tested in CI.
56
+ - **`AGENTS.md`:** a model-facing build guide (shipped in the npm files array), tuned over a 5-round agent-eval benchmark that lifted build quality from 2.78→4.51/5.
57
+
49
58
  ## Still missing from the full vision
50
59
 
51
60
  - First-party server runtime (`site/api/`), HTML-fragment `swap` semantics, and cart server sync — parked behind the adapter decision until a real project needs them.
52
- - Nested `:if` over loop items, and `@loop` over fetch sub-paths.
61
+ - `@loop` over fetch sub-paths (looping a path *inside* fetched state rather than top-level state).
53
62
  - `@starting-style` emission and browser fallbacks for older engines.
54
63
  - Editor tooling, richer diagnostics, and an actual npm publish (needs a license decision).
55
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/darkmown",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -13,12 +13,13 @@
13
13
  "scripts": {
14
14
  "build": "node src/cli.js build",
15
15
  "dev": "node src/cli.js dev",
16
- "test": "node --test"
16
+ "test": "node --test tests/*.test.js"
17
17
  },
18
18
  "files": [
19
19
  "src",
20
20
  "site",
21
21
  "docs",
22
+ "AGENTS.md",
22
23
  "README.md",
23
24
  "LICENSE",
24
25
  "CHANGELOG.md"
package/site/_/nav.skin CHANGED
@@ -11,7 +11,8 @@ tokens
11
11
 
12
12
  .topnav
13
13
  display flex
14
- gap 1rem
14
+ flex-wrap wrap
15
+ gap .5rem 1rem
15
16
  align-items center
16
17
  padding 1rem 2rem
17
18
  border-bottom 1px solid $line
package/site/_/nav.wd CHANGED
@@ -5,4 +5,5 @@
5
5
  <a href="/markdown/">Plain Markdown</a>
6
6
  <a href="/reactive/">Reactive</a>
7
7
  <a href="/data/">Data &amp; Forms</a>
8
+ <a href="/app/">Live app</a>
8
9
  </nav>
@@ -0,0 +1,7 @@
1
+ [
2
+ { "id": "loops", "title": "Nested conditionals in loops", "shipped": true, "blocked": false },
3
+ { "id": "editor", "title": "VS Code syntax highlighting", "shipped": true, "blocked": false },
4
+ { "id": "filter", "title": "State-driven list filtering", "shipped": false, "blocked": false },
5
+ { "id": "api", "title": "First-party site/api/ runtime", "shipped": false, "blocked": true },
6
+ { "id": "swap", "title": "HTML-fragment swap responses", "shipped": false, "blocked": false }
7
+ ]
@@ -0,0 +1,76 @@
1
+ tokens
2
+ ink #171717
3
+ paper #fbfaf7
4
+ panel #ffffff
5
+ line #e4e1d8
6
+ accent #16645a
7
+ warm #a83f2a
8
+ radius 10px
9
+
10
+ main
11
+ max-width 760px
12
+ margin 0 auto
13
+ padding 2.5rem 1.5rem 4rem
14
+ font ui-sans-serif, system-ui, sans-serif
15
+ color $ink
16
+
17
+ h1
18
+ font-size 2.1rem
19
+ letter-spacing -0.02em
20
+
21
+ .boundary
22
+ margin 1.5rem 0
23
+ padding 1rem 1.2rem
24
+ border-left 3px solid $warm
25
+ background #fcf4ef
26
+ border-radius $radius
27
+ font-size .95rem
28
+
29
+ .roadmap-row
30
+ display flex
31
+ align-items center
32
+ justify-content space-between
33
+ gap 1rem
34
+ padding .75rem 1rem
35
+ margin .5rem 0
36
+ background $panel
37
+ border 1px solid $line
38
+ border-radius $radius
39
+
40
+ .badge
41
+ font-size .8rem
42
+ font-weight 600
43
+ padding .2rem .6rem
44
+ border-radius 999px
45
+ white-space nowrap
46
+
47
+ .badge.shipped
48
+ background #e3f3ec
49
+ color $accent
50
+
51
+ .badge.wip
52
+ background #fef0e6
53
+ color $warm
54
+
55
+ .badge.blocked
56
+ background #f3e3e3
57
+ color #9a2a2a
58
+
59
+ button
60
+ background $accent
61
+ color white
62
+ border 0
63
+ padding .55rem .9rem
64
+ margin .25rem .4rem .25rem 0
65
+ border-radius 8px
66
+ font-size .95rem
67
+ cursor pointer
68
+
69
+ input
70
+ font-size 1rem
71
+ padding .6rem .7rem
72
+ border 1px solid $line
73
+ border-radius 8px
74
+ width 100%
75
+ max-width 22rem
76
+ margin .3rem 0
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: A live app in one Markdown file
3
+ description: One .wd file — static prose, fetched data, looped results with nested conditionals, a persistent counter, and a form that round-trips to a real server. No JavaScript written, no components, no build config.
4
+ ---
5
+
6
+ @include /nav.wd
7
+
8
+ <main>
9
+
10
+ # A live app, in one Markdown file
11
+
12
+ Everything on this page — the prose, the fetched list, the badges, the form, the counter — is a single `.wd` file. No components. No JSX. No build config. Watch where JavaScript actually enters.
13
+
14
+ ## Static until proven reactive
15
+
16
+ This whole section is plain Markdown. The list below is unrolled **at build time** from a JSON file — it renders into HTML with no runtime at all:
17
+
18
+ @loop /features.json into feature
19
+ - **{ feature.name }** — { feature.detail }
20
+ @endloop
21
+
22
+ <aside class="boundary">If you deleted everything below this line, this page would ship <strong>zero</strong> bytes of JavaScript. Everything above is static HTML. Below is where the one ~2&nbsp;KB runtime loads — and only because the document asks for it.</aside>
23
+
24
+ ## Fetched data, looped, with nested conditions
25
+
26
+ `:fetch` pulls JSON over the network into reactive state — from a static file here, or any API. Until it lands, the page shows a fallback. Each row then decides its own badge from **nested** conditionals, in plain Markdown:
27
+
28
+ :fetch roadmap from "/__wd/data/roadmap.json"
29
+
30
+ :if roadmap
31
+ @loop roadmap into item
32
+ ::: card .roadmap-row
33
+ **{ item.title }**
34
+ :if item.blocked
35
+ <span class="badge blocked">blocked</span>
36
+ :else
37
+ :if item.shipped
38
+ <span class="badge shipped">shipped ✓</span>
39
+ :else
40
+ <span class="badge wip">in progress</span>
41
+ :endif
42
+ :endif
43
+ :::
44
+ @endloop
45
+ :else
46
+ Loading the roadmap…
47
+ :endif
48
+
49
+ No ternaries, no `.map()`, no framework — the outer `:if` checks `blocked`, the inner one checks `shipped`, per row, and both stay reactive.
50
+
51
+ ## A counter that survives reload
52
+
53
+ `:state … persist` keeps this in `localStorage`, and `:computed` derives from it. Star a few, reload the page, and it holds:
54
+
55
+ ::: section #watch
56
+ :state stars = 0 persist
57
+ :computed milestone = stars >= 3
58
+
59
+ You are watching { stars } item(s).
60
+
61
+ :button "★ Watch one" -> stars++
62
+ :button "Reset" -> stars = 0
63
+
64
+ :if milestone
65
+ Nice — you are tracking the whole near-term roadmap.
66
+ :endif
67
+ :::
68
+
69
+ ## A form that talks to a real server
70
+
71
+ This form posts to a live serverless function and shows the reply. With JavaScript it is a `fetch`; without it, the exact same markup is a native POST — Darkmown adapts to your backend instead of owning one:
72
+
73
+ :form action="/__wd/echo" into reply
74
+ :input note placeholder="Tell us what you'd build with this"
75
+ :submit "Send to the server"
76
+ :endform
77
+
78
+ :if reply
79
+ The server got it and answered at **{ reply.at }**.
80
+ :else
81
+ Your reply will appear here.
82
+ :endif
83
+
84
+ :if reply_error
85
+ Something went wrong: { reply_error }
86
+ :endif
87
+
88
+ ---
89
+
90
+ That was one file. Static prose, build-time loops, fetched data, nested conditionals, persistent state, computed values, and a server round-trip — and the only JavaScript on the page is the shared runtime Darkmown loads for you. [Read the docs](/docs/) or [deploy your own](/#deploy).
91
+
92
+ </main>