@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 +1 -1
- package/CHANGELOG.md +24 -2
- package/README.md +15 -5
- package/docs/cli.md +2 -0
- package/docs/spec-alignment.md +8 -4
- package/package.json +17 -3
- package/site/_/nav.skin +7 -0
- package/site/pages/docs/index.wd +1 -1
- package/site/pages/index.skin +165 -6
- package/site/pages/index.wd +95 -6
- package/src/builder.js +60 -1
- package/src/cli.js +14 -1
- package/src/compiler.js +530 -21
- package/src/config.js +19 -0
- package/src/dev.js +9 -0
- package/src/router.js +26 -0
- package/src/runtime.js +129 -29
- package/src/scaffold.js +12 -0
- package/src/skin.js +31 -3
- package/src/statics.js +20 -0
- package/types/builder.d.ts +39 -0
- package/types/compiler.d.ts +184 -0
- package/types/config.d.ts +45 -0
- package/types/dev.d.ts +13 -0
- package/types/index.d.ts +33 -0
- package/types/router.d.ts +39 -0
- package/types/scaffold.d.ts +8 -0
- package/types/skin.d.ts +21 -0
- package/types/statics.d.ts +23 -0
- package/docs/okf-integration.md +0 -120
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.
|
|
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)** ·
|
|
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
|
|
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.
|
|
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
package/docs/spec-alignment.md
CHANGED
|
@@ -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` (~
|
|
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
|
|
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
|
|
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.
|
|
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
|
package/site/pages/docs/index.wd
CHANGED
|
@@ -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
|
package/site/pages/index.skin
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
48
|
-
max-width
|
|
48
|
+
margin .75rem 0 1rem
|
|
49
|
+
max-width 14ch
|
|
49
50
|
|
|
50
51
|
h2
|
|
51
|
-
margin-top
|
|
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
|
package/site/pages/index.wd
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Darkmown — Markdown that runs
|
|
3
|
-
description:
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
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>
|