@zvndev/darkmown 0.8.1 → 0.9.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 CHANGED
@@ -7,7 +7,7 @@ Darkmown is a Markdown-native web framework. You build sites by writing `.md` an
7
7
  - `.md` files are plain CommonMark. Directives stay inert text.
8
8
  - `.wd` files are the same Markdown **plus directives** (state, loops, conditionals, includes, forms).
9
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.
10
+ - Static pages ship **zero** JavaScript; a page loads the ~3.2 KB runtime only if it declares reactive behavior.
11
11
 
12
12
  ## Project layout & routing
13
13
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
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.9.0 — 2026-06-15
6
+
7
+ The "proof + full test pyramid" release: prove the moat on the homepage, test everything from unit to browser e2e, and pass a gold-standard open-source audit.
8
+
9
+ - **Landing: the vs-Markdoc proof.** The homepage is now a side-by-side comparison — the real `.wd` source for a live search-filter + add-to-cart store rendered working on the page, next to the equivalent Markdoc (which has no native loop and needs a React component + hydration for interactivity). The homepage is now itself a reactive `.wd` app; `/markdown/` and `/docs/` stay static (zero JS) to prove the static path.
10
+ - **Full test pyramid.** Unit + integration backfill (grammar whitelists, skin compiler, interpolation, router, every compile-error path), a zero-dependency CLI/pipeline e2e (`darkmown init` → build → serve → fetch → assert), real-browser e2e via Playwright (`npm run test:e2e`), and a seeded property/fuzz suite (5,000 cases/run) for the parser and expression validators. The zero-dep `npm test` suite grew from 98 to ~200 checks; coverage is ~96% lines, gated at 80% in CI.
11
+ - **The fuzzer earned its keep:** it found two real bugs, now fixed — two `:button` action errors that omitted the file path, and bare object interpolation (`{ meta }`) emitting `[object Object]` instead of an actionable error. Arrays now join with `, ` consistently.
12
+ - **Type safety.** Source stays plain JavaScript but is fully type-checked via JSDoc + `checkJs` (`npm run typecheck`, gated in CI), and ships `.d.ts` declarations. `typescript` is a dev-only dependency; the runtime stays zero-dependency.
13
+ - **Lean shipped runtime.** JSDoc is stripped from the runtime on emit, so the browser still downloads ~3.2 KB gzipped even though the source carries full type annotations. The size budget now measures the shipped artifact.
14
+ - **Gold-standard hardening (46.5/50 audit).** Release pack-smoke test before publish, `prepublishOnly` guard, `dependabot` (npm + GitHub Actions), CI `concurrency` cancellation, `.github/` PR + issue templates, `CODEOWNERS`, a short `CODE_OF_CONDUCT.md`, an exposed/documented `window.wd.debug`, and repo-root tidy-up.
15
+
16
+ ## 0.8.2 — 2026-06-14
17
+
18
+ Honesty, hardening, and polish pass — every claim in the docs is now backed by shipped code.
19
+
20
+ - **Security:** new per-page `html: false` frontmatter opt-out routes that page's Markdown through a strict renderer that escapes raw HTML — the mitigation for compiling untrusted/user-submitted content (default stays `html: true` for trusted authors). README and SECURITY.md now warn prominently that untrusted Markdown must be sanitized. `escapeHtml` also escapes single quotes.
21
+ - **Runtime:** state changes now batch into a single render per tick (`requestAnimationFrame`, falling back to a microtask), so rapid `:bind` input no longer re-runs the reconciler on every keystroke; the final state always renders and the public `wd.render()` stays synchronous. Opt-in `window.wd.debug` surfaces warnings for failed `:computed` / `where` expressions that were previously silent.
22
+ - **Tests:** first runtime/DOM test suite — keyed loop reconcile (node reuse on add/remove/reorder), `getPath` prototype-pollution rejection, loop-key collisions, and render batching — loading the real runtime via `node:vm`, no new dependencies. Suite grew 83 → 98.
23
+ - **Compiler:** unterminated frontmatter now throws an actionable error with the file path instead of silently swallowing the page; empty `---`/`---` blocks parse cleanly.
24
+ - **Docs & claims:** view transitions are now correctly described as parked/disabled (they were advertised but hardcoded off); the "no eval" wording is corrected (`:computed` and `where` compile to a whitelisted grammar run via `new Function`, never raw user content); runtime size corrected to the true ~3.1 KB gzipped across all surfaces; `:note`/`:try`/`:sprint` documented as demo-only; `docs/cli.md` scaffold output synced; VS Code install instructions corrected (build from source; Marketplace listing pending).
25
+ - **Site & tooling:** landing-page whitespace fixed (removed the forced `60vh` hero band); mobile nav no longer overflows; tagline locked to "Markdown that runs."; regenerated `package-lock.json` identity (`@zvndev/darkmown`).
26
+
5
27
  ## 0.8.1 — 2026-06-14
6
28
 
7
29
  - Fix: a quoted array item that follows `, ` no longer keeps a leading space — `tags: [a, "b, c"]` parses to `["a", "b, c"]`, not `["a", " b, c"]`.
@@ -33,7 +55,7 @@ All notable changes to Darkmown are documented here. Versions follow [semver](ht
33
55
  - **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.
34
56
  - **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.
35
57
  - 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.
36
- - 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.
58
+ - 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.
37
59
 
38
60
  ## 0.4.0 — 2026-06-11
39
61
 
@@ -57,7 +79,7 @@ All notable changes to Darkmown are documented here. Versions follow [semver](ht
57
79
  - `:if item.path` inside reactive loops (per-row branches).
58
80
  - `window.wd` escape hatch for colocated `.js`.
59
81
  - `::: section` containers with scoped state; keyed loop reconciliation.
60
- - View transitions via `transitions: true` frontmatter.
82
+ - View transitions via `transitions: true` frontmatter. **(Disabled since: cross-document `@view-transition` render-blocked deployed pages, hanging navigation. `transitions` is currently hardcoded off in `src/compiler.js`; reintroduction is tracked pending proper activation fallbacks.)**
61
83
  - Real CommonMark parsing (markdown-it); strict `.md` (directives stay plain text).
62
84
  - `@loop … into … @endloop` — the one loop, replacing `@repeat` and `:for`.
63
85
  - Dev server rebuilds in a child process (always-fresh modules).
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Darkmown
2
2
 
3
- **[darkmown.com](https://darkmown.com)** · markdown, rearranged.
3
+ **[darkmown.com](https://darkmown.com)** · Markdown that runs.
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 around 2 KB gzipped (CI-enforced under 5 KB).
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 ~3.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 ~2 KB gzipped, CI-enforced under 5 KB).
48
+ - Static pages ship zero Darkmown runtime. Reactive pages share `/__wd/runtime.js` (currently ~3.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
@@ -100,7 +100,7 @@ Add `where <predicate>` to filter a loop. Conditions compare a loop-item field a
100
100
  @endloop
101
101
  ```
102
102
 
103
- 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`).
103
+ 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). Raw user content is never evaluated; the validated predicate compiles to a whitelisted grammar that runs via `new Function`.
104
104
 
105
105
  **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:
106
106
 
@@ -209,9 +209,19 @@ Loading…
209
209
 
210
210
  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`.
211
211
 
212
+ Set `window.wd.debug = true` (it defaults to `false`) to log any `:computed` or `@loop … where` expression that fails to evaluate to the console — useful while authoring reactive pages.
213
+
212
214
  ## Editor support
213
215
 
214
- 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").
216
+ 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. Install it from source by building a `.vsix`: `cd editors/vscode && npx @vscode/vsce package`, then `code --install-extension darkmown-*.vsix`. A Marketplace listing is coming soon.
217
+
218
+ ## Security
219
+
220
+ Darkmown renders Markdown with `html: true`, so **raw HTML in your content passes through verbatim** — by design, like every Markdown site generator. Treat content files as trusted input.
221
+
222
+ > **The single biggest footgun:** do not compile untrusted or user-submitted Markdown without sanitizing it first. There is no built-in sanitizer.
223
+
224
+ Directive actions and `:computed`/`@loop … where` expressions are never `eval`'d as raw user content — they compile to a whitelisted grammar (item paths, declared `:state`, numbers, strings) that runs via `new Function`. See [SECURITY.md](SECURITY.md) for the full security model.
215
225
 
216
226
  ## Spec status
217
227
 
package/docs/cli.md CHANGED
@@ -16,8 +16,10 @@ darkmown help
16
16
 
17
17
  Creates a minimal Darkmown site with:
18
18
 
19
+ - `package.json` (pins the current `@zvndev/darkmown` version)
19
20
  - `site/pages/index.wd`
20
21
  - `site/pages/index.skin`
22
+ - `site/pages/about.md`
21
23
  - `site/_/nav.wd`
22
24
  - `README.md`
23
25
 
@@ -6,7 +6,7 @@ This audit compares the current Darkmown implementation with the original MARROW
6
6
 
7
7
  - **Markdown-first authoring:** `.md` is strict CommonMark rendered by a real parser (markdown-it); `.wd` is the same Markdown plus first-party directives. Renaming the file is the upgrade path — extensions genuinely gate functionality.
8
8
  - **Content tree over components:** routing, includes, and repeated fragments are file/document based instead of React-style components.
9
- - **Tiered output:** static routes emit HTML/CSS and no Darkmown runtime; reactive routes opt into `/__wd/runtime.js` (~1.1 KB gzipped, below the original 5 KB target).
9
+ - **Tiered output:** static routes emit HTML/CSS and no Darkmown runtime; reactive routes opt into `/__wd/runtime.js` (~3.2 KB gzipped, CI-enforced below the 5 KB target).
10
10
  - **No virtual DOM:** state changes patch direct DOM bindings, conditional regions, and keyed loop regions.
11
11
  - **Keyed list patching:** reactive loops reconcile by item key (`id`/`key`/value) instead of re-rendering the region.
12
12
  - **Sections with scoped state:** `::: section #id .class` containers scope `:state`; bindings and actions resolve through the nearest scope chain.
@@ -33,7 +33,7 @@ This audit compares the current Darkmown implementation with the original MARROW
33
33
 
34
34
  ## Stage 4 additions (2026-06-11, same day)
35
35
 
36
- - **View transitions:** `transitions: true` in frontmatter emits `@view-transition { navigation: auto; }` — the original doc's zero-JS MPA navigation polish.
36
+ - **View transitions (currently disabled):** `transitions: true` in frontmatter was wired to emit `@view-transition { navigation: auto; }` — the original doc's zero-JS MPA navigation polish. **This is turned off as of now:** cross-document `@view-transition` render-blocked deployed pages (stalled rAF, hanging navigation), so `transitions` is hardcoded to `""` in `src/compiler.js`. The frontmatter key is accepted but has no effect today. Reintroduction is tracked pending proper activation fallbacks (see "Still missing from the full vision").
37
37
  - **Lazy loading:** `:fetch … when=visible` defers the request until the marker scrolls into view (IntersectionObserver) — the adapted form of `:lazy`.
38
38
  - **Computed state:** `:computed name = expr` with a compile-time-validated grammar (state refs, numbers, strings, arithmetic, comparisons; assignment, calls, and prototype walks rejected). Initial values are evaluated at build time.
39
39
  - **Dev self-reload:** `darkmown dev` rebuilds in a child process, so changes to the framework's own `src/` always compile with fresh modules.
@@ -49,7 +49,7 @@ This audit compares the current Darkmown implementation with the original MARROW
49
49
  ## Stage 6 additions (2026-06-13)
50
50
 
51
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.
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. Raw user content is never evaluated; the validated predicate compiles to the whitelisted grammar and runs via `new Function` at runtime. 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
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
54
  - **Per-row actions in reactive loops:** a `:button` inside `@loop … into item` can act on its own row — `cart += item` appends a copy of the row to another `:state` list (the only way to carry a loop item into another list), and `list remove item` removes the row from the looped `:state` source (both names compile-checked against the enclosing loop). The runtime resolves the clicked row by stamping each reconciled node with its item, so removal is exact even under a `where` filter. This closes the last "store demo" gap noted in the design memory (per-row `:button` couldn't carry row data); `:if` string-equality remains the only listed gap, now partly covered by `@loop … where`.
55
55
  - **`.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.
@@ -60,11 +60,15 @@ This audit compares the current Darkmown implementation with the original MARROW
60
60
 
61
61
  - **Array frontmatter + readable `meta`:** `parseFrontmatter` now parses inline flow arrays (`tags: [a, b, "x, y"]` → real array; quoted items keep internal commas; a value without a leading `[` stays a scalar — block sequences are intentionally out of scope to keep the parser single-pass). A page's frontmatter is exposed to its body under `meta`, so `{ meta.title }` prints a field, `{ meta.tags }` renders an array joined with `, `, and `@loop meta.tags into tag` iterates a frontmatter array at build time (stays `runtime: false`). Genuine correctness fix on its own, and the groundwork for reading/emitting OKF-style bundles (Google Cloud's Open Knowledge Format — markdown + `type`-bearing frontmatter + cross-links; tracked but deliberately not coupled to its v0.1 Draft).
62
62
 
63
+ ## Demo-only directives (not part of the public surface)
64
+
65
+ The compiler recognizes three directives — `:note`, `:try`, and `:sprint` — that exist **only to power the demo site** (darkmown.com). They are **not** part of the supported public directive set, are not documented for authors, and may change or be removed without notice. Do not build pages that depend on them. The supported public directives are the ones catalogued above: `:state`, `:computed`, `:button`, `:bind`, `:if`/`:else`/`:endif`, `:fetch`, `:form`/`:input`/`:submit`, `@loop`/`@endloop` (with `where`), `@include`, and `::: section`.
66
+
63
67
  ## Still missing from the full vision
64
68
 
65
69
  - 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.
66
70
  - `@loop` over fetch sub-paths (looping a path *inside* fetched state rather than top-level state).
67
- - `@starting-style` emission and browser fallbacks for older engines.
71
+ - **View transitions:** re-enabling `transitions: true` with proper activation fallbacks (disabled today — see Stage 4), plus `@starting-style` emission and browser fallbacks for older engines.
68
72
  - Editor tooling, richer diagnostics, and an actual npm publish (needs a license decision).
69
73
 
70
74
  ## Current claim
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/darkmown",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -10,15 +10,23 @@
10
10
  "bin": {
11
11
  "darkmown": "./src/cli.js"
12
12
  },
13
+ "types": "./types/index.d.ts",
13
14
  "scripts": {
14
15
  "build": "node src/cli.js build",
15
16
  "dev": "node src/cli.js dev",
16
- "test": "node --test tests/*.test.js"
17
+ "test": "node --test tests/*.test.js",
18
+ "test:cov": "node --test --experimental-test-coverage tests/*.test.js",
19
+ "test:e2e": "playwright test",
20
+ "typecheck": "tsc --noEmit",
21
+ "types": "tsc -p tsconfig.types.json",
22
+ "prepublishOnly": "npm test"
17
23
  },
18
24
  "files": [
19
25
  "src",
26
+ "types",
20
27
  "site",
21
- "docs",
28
+ "docs/cli.md",
29
+ "docs/spec-alignment.md",
22
30
  "AGENTS.md",
23
31
  "README.md",
24
32
  "LICENSE",
@@ -30,6 +38,12 @@
30
38
  "dependencies": {
31
39
  "markdown-it": "^14.2.0"
32
40
  },
41
+ "devDependencies": {
42
+ "@playwright/test": "^1.49.0",
43
+ "@types/markdown-it": "^14.1.2",
44
+ "@types/node": "^22.19.21",
45
+ "typescript": "^5.9.3"
46
+ },
33
47
  "repository": {
34
48
  "type": "git",
35
49
  "url": "git+https://github.com/ZVN-DEV/darkmown.git"
package/site/_/nav.skin CHANGED
@@ -15,6 +15,8 @@ tokens
15
15
  gap .5rem 1rem
16
16
  align-items center
17
17
  padding 1rem 2rem
18
+ max-width 100%
19
+ box-sizing border-box
18
20
  border-bottom 1px solid $line
19
21
  bg rgba(251,250,247,.85)
20
22
  backdrop-filter blur(8px)
@@ -22,6 +24,11 @@ tokens
22
24
  top 0
23
25
  z-index 10
24
26
 
27
+ @media (max-width: 600px)
28
+ .topnav
29
+ padding .85rem 1rem
30
+ gap .4rem .85rem
31
+
25
32
  .topnav strong
26
33
  color $warm
27
34
  font-size 1.05rem
@@ -131,7 +131,7 @@ tags: [sales, revenue, "q1, q2"]
131
131
 
132
132
  ## Reactive directives
133
133
 
134
- Reactive pages opt into `/__wd/runtime.js` (~2 KB gzipped, under a CI-enforced 5 KB budget). Static pages do not.
134
+ Reactive pages opt into `/__wd/runtime.js` (~3.2 KB gzipped, under a CI-enforced 5 KB budget). Static pages do not.
135
135
 
136
136
  ```wd
137
137
  :state count = 0
@@ -18,16 +18,17 @@ page
18
18
  padding 0 0 4rem
19
19
 
20
20
  .hero
21
- min-height 60vh
22
21
  display flex
23
22
  flex-direction column
24
23
  align-items flex-start
25
24
  justify-content center
25
+ padding 2.5rem 0 2.25rem
26
26
  border-bottom 1px solid $line
27
- margin-bottom 2rem
27
+ margin-bottom 2.5rem
28
28
 
29
29
  .hero > p:not(.eyebrow)
30
30
  max-width 70ch
31
+ margin .75rem 0 1.5rem
31
32
 
32
33
  .eyebrow
33
34
  color $accent
@@ -39,16 +40,16 @@ page
39
40
  main
40
41
  max-width 920px
41
42
  margin 0 auto
42
- padding 1rem 2rem 2rem
43
+ padding 0 2rem 2rem
43
44
 
44
45
  h1
45
46
  font-size clamp(2.4rem, 6vw, 5rem)
46
47
  line-height .95
47
- margin 2rem 0 1rem
48
- max-width 10ch
48
+ margin .75rem 0 1rem
49
+ max-width 14ch
49
50
 
50
51
  h2
51
- margin-top 2rem
52
+ margin-top 2.75rem
52
53
  border-bottom 1px solid $line
53
54
  padding-bottom .35rem
54
55
 
@@ -132,3 +133,161 @@ a
132
133
  display block
133
134
  color $accent
134
135
  font-size .9rem
136
+
137
+ /* ── vs-Markdoc comparison ───────────────────────────────────────── */
138
+
139
+ .versus
140
+ display grid
141
+ grid-template-columns 1fr 1fr
142
+ gap 1.25rem
143
+ align-items start
144
+ margin 2.5rem 0 1rem
145
+
146
+ .vs-card
147
+ border 1px solid $line
148
+ radius $radius
149
+ bg $panel
150
+ padding 1.25rem 1.25rem 1.1rem
151
+ display flex
152
+ flex-direction column
153
+ min-width 0
154
+
155
+ .vs-card.vs-dark
156
+ bg #fbf7ef
157
+ border 1px solid #e3d8c4
158
+ box-shadow 0 1px 0 #efe7d6
159
+
160
+ .vs-head
161
+ display flex
162
+ align-items baseline
163
+ justify-content space-between
164
+ gap .75rem
165
+ margin-bottom .85rem
166
+
167
+ .vs-tag
168
+ font-family ui-monospace, SFMono-Regular, Menlo, monospace
169
+ font-size .72rem
170
+ font-weight 700
171
+ text-transform uppercase
172
+ letter-spacing .08em
173
+ padding .25rem .55rem
174
+ radius 999px
175
+
176
+ .vs-tag-wd
177
+ color white
178
+ bg $accent
179
+
180
+ .vs-tag-md
181
+ color $muted
182
+ bg #efece3
183
+ border 1px solid $line
184
+
185
+ .vs-sub
186
+ font-family ui-monospace, SFMono-Regular, Menlo, monospace
187
+ font-size .82rem
188
+ color $muted
189
+
190
+ .vs-card pre
191
+ margin .25rem 0 0
192
+ font-size .82rem
193
+ line-height 1.55
194
+
195
+ .vs-live-label
196
+ margin 1rem 0 .35rem
197
+ font-size .88rem
198
+ color $muted
199
+ font-style italic
200
+
201
+ .vs-live
202
+ border 1px dashed #cdbfa6
203
+ radius $radius
204
+ bg #fffdf8
205
+ padding 1rem 1.1rem .85rem
206
+ margin .15rem 0 0
207
+
208
+ .vs-live input[data-wd-bind-input]
209
+ width 100%
210
+ box-sizing border-box
211
+ font inherit
212
+ font-size .95rem
213
+ padding .55rem .7rem
214
+ border 1px solid $line
215
+ radius 6px
216
+ bg white
217
+ margin 0 0 .75rem
218
+
219
+ .vs-live .product
220
+ display flex
221
+ align-items center
222
+ justify-content space-between
223
+ gap .75rem
224
+ padding .5rem .7rem
225
+ margin .35rem 0
226
+ bg $panel
227
+ border 1px solid $line
228
+ radius 6px
229
+ font-size .95rem
230
+
231
+ .vs-live .product p
232
+ margin 0
233
+
234
+ .vs-live .product button
235
+ border 0
236
+ bg $accent
237
+ color white
238
+ font inherit
239
+ font-size .82rem
240
+ padding .35rem .7rem
241
+ radius 6px
242
+ cursor pointer
243
+ flex-shrink 0
244
+
245
+ .vs-live .product button:hover
246
+ bg #11514a
247
+
248
+ .vs-live > p:last-of-type
249
+ margin .85rem 0 0
250
+ font-size .95rem
251
+
252
+ .vs-needs
253
+ margin 1rem 0 0
254
+ padding 1rem 1.1rem
255
+ border-left 3px solid $warm
256
+ bg #fcf4ef
257
+ radius 6px
258
+
259
+ .vs-needs strong
260
+ color $warm
261
+ font-size .92rem
262
+
263
+ .vs-needs pre
264
+ margin .7rem 0 .6rem
265
+ font-size .78rem
266
+
267
+ .vs-needs-foot
268
+ display block
269
+ font-size .85rem
270
+ color $muted
271
+
272
+ .vs-caption
273
+ margin auto 0 0
274
+ padding-top 1rem
275
+ font-style italic
276
+ color $ink
277
+ font-size .95rem
278
+
279
+ .vs-card.vs-dark .vs-caption
280
+ color $accent
281
+
282
+ /* Secondary value-prop row, demoted below the comparison */
283
+
284
+ .props
285
+ margin 1.5rem 0 0
286
+
287
+ .props h2
288
+ margin-top 1.5rem
289
+
290
+ @media (max-width: 720px)
291
+ .versus
292
+ grid-template-columns 1fr
293
+ gap 1rem
@@ -1,6 +1,6 @@
1
1
  ---
2
- title: Darkmown — Markdown that runs
3
- description: Build full websites in plain Markdown. Routing, state, server calls, includes, and reactive islands — no JavaScript or CSS required. Your docs and your code are all just text.
2
+ title: Darkmown — Markdown that runs, not Markdown that needs React
3
+ 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.
4
4
  ---
5
5
 
6
6
  @include /nav.wd
@@ -8,17 +8,105 @@ description: Build full websites in plain Markdown. Routing, state, server calls
8
8
  <main>
9
9
 
10
10
  <section class="hero">
11
- <p class="eyebrow">The Markdown framework</p>
11
+ <p class="eyebrow">Darkmown vs. Markdoc</p>
12
12
 
13
- # Markdown that runs
13
+ # Markdown that runs. Not Markdown that needs React.
14
14
 
15
- Not just pages — **full websites**. Routing, state, server calls, embeds, and reactive islands, all from plain Markdown files. No JavaScript or CSS required. Your docs and your code are all just text.
15
+ The left file is Markdown. It is also a running app — a live search filter, add-to-cart, and a running total. The right file is the closest Markdoc can get on its own: a static list. To make it do what the left one already does, you add a React component, hydration, and a build step.
16
16
 
17
17
  <a class="btn primary" href="/app/">See a whole app in one file</a>
18
18
  <a class="btn" href="/docs/">Read the docs</a>
19
19
  </section>
20
20
 
21
- :note "Static pages ship zero JavaScript. The moment a page asks for reactivity, one ~2 KB runtime loads — never before. JS optional, CSS optional, always."
21
+ <section class="versus">
22
+
23
+ ::: div .vs-card .vs-dark
24
+ <header class="vs-head"><span class="vs-tag vs-tag-wd">Darkmown</span><span class="vs-sub">store.wd</span></header>
25
+
26
+ ```wd
27
+ :state products = [{"id":1,"name":"Aurora Lamp","price":49},{"id":2,"name":"Briza Fan","price":39},{"id":3,"name":"Cove Speaker","price":89},{"id":4,"name":"Dune Mug","price":12}]
28
+ :state cart = []
29
+ :state q = ""
30
+ :computed count = cart.length
31
+
32
+ :bind q placeholder="Search products…"
33
+
34
+ @loop products into p where p.name contains q
35
+ ::: card .product
36
+ **{ p.name }** — ${ p.price }
37
+ :button "Add" -> cart += p
38
+ :::
39
+ @endloop
40
+
41
+ Cart: { count } item(s)
42
+ ```
43
+
44
+ <p class="vs-live-label">↓ and here is that exact file, running</p>
45
+
46
+ ::: div .vs-live
47
+ :state products = [{"id":1,"name":"Aurora Lamp","price":49},{"id":2,"name":"Briza Fan","price":39},{"id":3,"name":"Cove Speaker","price":89},{"id":4,"name":"Dune Mug","price":12}]
48
+ :state cart = []
49
+ :state q = ""
50
+ :computed count = cart.length
51
+
52
+ :bind q placeholder="Search products…"
53
+
54
+ @loop products into p where p.name contains q
55
+ ::: card .product
56
+ **{ p.name }** — ${ p.price }
57
+ :button "Add" -> cart += p
58
+ :::
59
+ @endloop
60
+
61
+ Cart: **{ count }** item(s)
62
+ :::
63
+
64
+ <p class="vs-caption">This is the whole file. It runs.</p>
65
+ :::
66
+
67
+ ::: div .vs-card
68
+ <header class="vs-head"><span class="vs-tag vs-tag-md">Markdoc</span><span class="vs-sub">store.md</span></header>
69
+
70
+ ```text
71
+ {% if $products %}
72
+ - **Aurora Lamp** — $49
73
+ - **Briza Fan** — $39
74
+ - **Cove Speaker** — $89
75
+ - **Dune Mug** — $12
76
+ {% /if %}
77
+ ```
78
+
79
+ <p class="vs-live-label">Markdoc has <code>{% if %}</code>, <code>{% partial %}</code>, and <code>$variables</code> — but 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:</p>
80
+
81
+ ::: div .vs-needs
82
+ <strong>+ store-widget.jsx</strong> — a React component, registered as a custom tag:
83
+
84
+ ```jsx
85
+ const tags = { store: { render: 'Store' } };
86
+ Markdoc.renderers.react(content, React, {
87
+ components: { Store } // your filter + cart + total live here
88
+ });
89
+ ```
90
+
91
+ <span class="vs-needs-foot">…plus a React + hydration runtime shipped to the browser, and the build step that wires the tag to the component.</span>
92
+ :::
93
+
94
+ <p class="vs-caption">Same outcome needs a React component.</p>
95
+ :::
96
+
97
+ </section>
98
+
99
+ :note "Static pages ship zero JavaScript. The moment a page asks for reactivity — like this very homepage — one ~3.2 KB runtime loads, never before. JS optional, CSS optional, always."
100
+
101
+ <section class="props">
102
+
103
+ ## Three rules, no escape hatch
104
+
105
+ - **Zero-JS static.** A page with no state ships no framework JavaScript. `/markdown/` and `/docs/` prove it — open them and check the network tab.
106
+ - **One runtime.** Every reactive page shares the same ~3.2 KB runtime. No per-component bundles, no hydration tax that scales with your UI.
107
+ - **One loop, one binding.** `@loop … into …` is the only loop; `{ name }` is the only interpolation. Nothing else to learn, nothing else to misuse.
108
+
109
+ </section>
22
110
 
23
111
  ## A whole framework, in text
24
112
 
@@ -78,3 +166,4 @@ Darkmown itself was built by a small agent team. The board below is a live `@loo
78
166
  @include /sprint-loop.wd
79
167
 
80
168
  </main>
169
+ </content>