@zvndev/darkmown 0.8.1 → 0.8.2
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 +13 -2
- package/README.md +13 -5
- package/docs/cli.md +2 -0
- package/docs/spec-alignment.md +8 -4
- package/package.json +1 -1
- package/site/_/nav.skin +7 -0
- package/site/pages/docs/index.wd +1 -1
- package/site/pages/index.skin +7 -6
- package/site/pages/index.wd +2 -2
- package/src/compiler.js +28 -8
- package/src/runtime.js +31 -7
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 ~
|
|
10
|
+
- Static pages ship **zero** JavaScript; a page loads the ~3.1 KB runtime only if it declares reactive behavior.
|
|
11
11
|
|
|
12
12
|
## Project layout & routing
|
|
13
13
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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.8.2 — 2026-06-14
|
|
6
|
+
|
|
7
|
+
Honesty, hardening, and polish pass — every claim in the docs is now backed by shipped code.
|
|
8
|
+
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **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.
|
|
12
|
+
- **Compiler:** unterminated frontmatter now throws an actionable error with the file path instead of silently swallowing the page; empty `---`/`---` blocks parse cleanly.
|
|
13
|
+
- **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).
|
|
14
|
+
- **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`).
|
|
15
|
+
|
|
5
16
|
## 0.8.1 — 2026-06-14
|
|
6
17
|
|
|
7
18
|
- 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 +44,7 @@ All notable changes to Darkmown are documented here. Versions follow [semver](ht
|
|
|
33
44
|
- **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
45
|
- **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
46
|
- 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.
|
|
47
|
+
- 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
48
|
|
|
38
49
|
## 0.4.0 — 2026-06-11
|
|
39
50
|
|
|
@@ -57,7 +68,7 @@ All notable changes to Darkmown are documented here. Versions follow [semver](ht
|
|
|
57
68
|
- `:if item.path` inside reactive loops (per-row branches).
|
|
58
69
|
- `window.wd` escape hatch for colocated `.js`.
|
|
59
70
|
- `::: section` containers with scoped state; keyed loop reconciliation.
|
|
60
|
-
- View transitions via `transitions: true` frontmatter.
|
|
71
|
+
- 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
72
|
- Real CommonMark parsing (markdown-it); strict `.md` (directives stay plain text).
|
|
62
73
|
- `@loop … into … @endloop` — the one loop, replacing `@repeat` and `:for`.
|
|
63
74
|
- 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
|
|
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.1 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 ~
|
|
48
|
+
- Static pages ship zero Darkmown runtime. Reactive pages share `/__wd/runtime.js` (currently ~3.1 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
|
|
|
@@ -211,7 +211,15 @@ Reactive pages expose `window.wd` — `wd.get(key)`, `wd.set(key, value)`, `wd.s
|
|
|
211
211
|
|
|
212
212
|
## Editor support
|
|
213
213
|
|
|
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.
|
|
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. 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.
|
|
215
|
+
|
|
216
|
+
## Security
|
|
217
|
+
|
|
218
|
+
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.
|
|
219
|
+
|
|
220
|
+
> **The single biggest footgun:** do not compile untrusted or user-submitted Markdown without sanitizing it first. There is no built-in sanitizer.
|
|
221
|
+
|
|
222
|
+
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
223
|
|
|
216
224
|
## Spec status
|
|
217
225
|
|
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.1 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
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` (~
|
|
134
|
+
Reactive pages opt into `/__wd/runtime.js` (~3.1 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
|
|
package/site/pages/index.wd
CHANGED
|
@@ -10,7 +10,7 @@ description: Build full websites in plain Markdown. Routing, state, server calls
|
|
|
10
10
|
<section class="hero">
|
|
11
11
|
<p class="eyebrow">The Markdown framework</p>
|
|
12
12
|
|
|
13
|
-
# Markdown that runs
|
|
13
|
+
# Markdown that runs.
|
|
14
14
|
|
|
15
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.
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ Not just pages — **full websites**. Routing, state, server calls, embeds, and
|
|
|
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 ~
|
|
21
|
+
:note "Static pages ship zero JavaScript. The moment a page asks for reactivity, one ~3.1 KB runtime loads — never before. JS optional, CSS optional, always."
|
|
22
22
|
|
|
23
23
|
## A whole framework, in text
|
|
24
24
|
|
package/src/compiler.js
CHANGED
|
@@ -5,6 +5,20 @@ import MarkdownIt from "markdown-it";
|
|
|
5
5
|
const md = new MarkdownIt({ html: true });
|
|
6
6
|
md.use(bindingPlugin);
|
|
7
7
|
|
|
8
|
+
// Raw HTML in markdown passes through by default (a documented design choice).
|
|
9
|
+
// Pages can opt out with frontmatter `html: false` for untrusted content; the
|
|
10
|
+
// stricter instance is built lazily so the default path stays a single instance.
|
|
11
|
+
let mdNoHtml = null;
|
|
12
|
+
function selectMd(meta) {
|
|
13
|
+
// Frontmatter scalars stay strings (no coercion), so accept both forms.
|
|
14
|
+
if (meta?.html !== false && meta?.html !== "false") return md;
|
|
15
|
+
if (!mdNoHtml) {
|
|
16
|
+
mdNoHtml = new MarkdownIt({ html: false });
|
|
17
|
+
mdNoHtml.use(bindingPlugin);
|
|
18
|
+
}
|
|
19
|
+
return mdNoHtml;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
const pageIncludeExtensions = [".md", ".wd"];
|
|
9
23
|
|
|
10
24
|
export function compilePage(file, context) {
|
|
@@ -74,24 +88,27 @@ function compileFile(file, context, stack, scope, comp, sections, loopItem) {
|
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
const raw = fs.readFileSync(file, "utf8");
|
|
77
|
-
const { meta, body } = parseFrontmatter(raw);
|
|
91
|
+
const { meta, body } = parseFrontmatter(raw, file);
|
|
78
92
|
collectColocatedAssets(file, context, comp.assets);
|
|
79
93
|
|
|
80
94
|
if (path.extname(file) === ".md") {
|
|
81
95
|
scanMarkdownHints(body, file, comp);
|
|
82
|
-
return { meta, html:
|
|
96
|
+
return { meta, html: selectMd(meta).render(body, {}) };
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
// Expose this file's frontmatter to the body under `meta` so `{ meta.title }`,
|
|
86
100
|
// `{ meta.tags }`, and `@loop meta.tags into tag` resolve as static values.
|
|
87
|
-
const ctx = { file, context, stack: [...stack, real], scope: createScope(scope, { meta }), comp, sections, loopItem };
|
|
101
|
+
const ctx = { file, context, stack: [...stack, real], scope: createScope(scope, { meta }), comp, sections, loopItem, md: selectMd(meta) };
|
|
88
102
|
return { meta, html: compileBody(body.replace(/\r\n?/g, "\n").split("\n"), ctx) };
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
export function parseFrontmatter(raw) {
|
|
105
|
+
export function parseFrontmatter(raw, file) {
|
|
92
106
|
if (!raw.startsWith("---\n")) return { meta: {}, body: raw };
|
|
93
|
-
const end = raw.indexOf("\n---",
|
|
94
|
-
if (end === -1)
|
|
107
|
+
const end = raw.indexOf("\n---", 3);
|
|
108
|
+
if (end === -1) {
|
|
109
|
+
const where = file ? ` in ${file}` : "";
|
|
110
|
+
throw new Error(`Unterminated frontmatter${where}: opening "---" has no closing "---". Use: --- on its own line to open and another --- to close, then the page body.`);
|
|
111
|
+
}
|
|
95
112
|
const front = raw.slice(4, end).trim();
|
|
96
113
|
const body = raw.slice(end + 4).replace(/^\n/, "");
|
|
97
114
|
const meta = {};
|
|
@@ -462,6 +479,7 @@ function handleComputed(line, ctx) {
|
|
|
462
479
|
const read = (key, path) => getPath(ctx.comp.state.get(key), path ? path.split(".") : []);
|
|
463
480
|
initial = new Function("S", `return (${expr});`)(read);
|
|
464
481
|
} catch {
|
|
482
|
+
console.warn(`:computed "${match[2].trim()}" in ${ctx.file} could not be evaluated at build time; falling back to null. Check the expression.`);
|
|
465
483
|
initial = null;
|
|
466
484
|
}
|
|
467
485
|
const key = declareState(match[1], initial ?? null, ctx);
|
|
@@ -731,6 +749,7 @@ function evalPredicate(body, item, ctx) {
|
|
|
731
749
|
const S = (k, r) => getPath(ctx.comp.state.get(k), r ? r.split(".") : []);
|
|
732
750
|
return Boolean(new Function("I", "S", "C", `return (${body});`)(I, S, containsHelper));
|
|
733
751
|
} catch {
|
|
752
|
+
console.warn(`@loop where predicate "${body}" in ${ctx.file} could not be evaluated at build time; treating the row as excluded. Check the condition.`);
|
|
734
753
|
return false;
|
|
735
754
|
}
|
|
736
755
|
}
|
|
@@ -878,7 +897,7 @@ function renderDemoDirective(line) {
|
|
|
878
897
|
// ---------------------------------------------------------------------------
|
|
879
898
|
|
|
880
899
|
function renderProse(text, ctx) {
|
|
881
|
-
return md.render(text, { resolveBinding: (expr) => resolveBindingHtml(expr, ctx) });
|
|
900
|
+
return (ctx.md ?? md).render(text, { resolveBinding: (expr) => resolveBindingHtml(expr, ctx) });
|
|
882
901
|
}
|
|
883
902
|
|
|
884
903
|
function bindingPlugin(mdInstance) {
|
|
@@ -1141,5 +1160,6 @@ export function escapeHtml(value) {
|
|
|
1141
1160
|
.replaceAll("&", "&")
|
|
1142
1161
|
.replaceAll("<", "<")
|
|
1143
1162
|
.replaceAll(">", ">")
|
|
1144
|
-
.replaceAll('"', """)
|
|
1163
|
+
.replaceAll('"', """)
|
|
1164
|
+
.replaceAll("'", "'");
|
|
1145
1165
|
}
|
package/src/runtime.js
CHANGED
|
@@ -36,16 +36,22 @@ function getPath(value, path) {
|
|
|
36
36
|
|
|
37
37
|
const computedDefs = [...document.querySelectorAll("[data-wd-computed]")].map((node) => ({
|
|
38
38
|
key: node.getAttribute("data-wd-computed-key"),
|
|
39
|
+
expr: node.getAttribute("data-wd-computed-expr"),
|
|
39
40
|
evaluate: new Function("S", `return (${node.getAttribute("data-wd-computed-expr")});`)
|
|
40
41
|
}));
|
|
41
42
|
|
|
43
|
+
function warn(expr, error) {
|
|
44
|
+
if (window.wd && window.wd.debug) console.warn(`[wd] expression failed: ${expr}`, error);
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
function recompute() {
|
|
43
48
|
const read = (key, path) => getPath(state[key], path);
|
|
44
49
|
for (const def of computedDefs) {
|
|
45
50
|
try {
|
|
46
51
|
state[def.key] = def.evaluate(read);
|
|
47
|
-
} catch {
|
|
52
|
+
} catch (error) {
|
|
48
53
|
state[def.key] = undefined;
|
|
54
|
+
warn(def.expr, error);
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
}
|
|
@@ -89,7 +95,7 @@ function fillItem(node, item) {
|
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
function
|
|
98
|
+
function renderNow() {
|
|
93
99
|
recompute();
|
|
94
100
|
for (const node of document.querySelectorAll("[data-wd-if]")) {
|
|
95
101
|
const value = getPath(state[node.getAttribute("data-wd-if")], node.getAttribute("data-wd-path"));
|
|
@@ -112,9 +118,14 @@ function render() {
|
|
|
112
118
|
const where = region.getAttribute("data-wd-loop-where");
|
|
113
119
|
if (where) {
|
|
114
120
|
const predicate = loopPredicate(where);
|
|
115
|
-
list = list.filter((item) =>
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
list = list.filter((item) => {
|
|
122
|
+
try {
|
|
123
|
+
return predicate((path) => getPath(item, path), (k, r) => getPath(state[k], r || ""), containsFn);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
warn(where, error);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
const existing = new Map();
|
|
@@ -149,6 +160,19 @@ function render() {
|
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
// Coalesce rapid state mutations into one render on the next tick. Each scheduled
|
|
164
|
+
// pass always reads the latest state, so the final update is never dropped.
|
|
165
|
+
const schedule = typeof requestAnimationFrame === "function" ? requestAnimationFrame : queueMicrotask;
|
|
166
|
+
let scheduled = false;
|
|
167
|
+
function render() {
|
|
168
|
+
if (scheduled) return;
|
|
169
|
+
scheduled = true;
|
|
170
|
+
schedule(() => {
|
|
171
|
+
scheduled = false;
|
|
172
|
+
renderNow();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
152
176
|
document.addEventListener("input", (event) => {
|
|
153
177
|
const input = event.target.closest("[data-wd-bind-input]");
|
|
154
178
|
if (!input) return;
|
|
@@ -274,7 +298,7 @@ window.wd = {
|
|
|
274
298
|
savePersisted();
|
|
275
299
|
render();
|
|
276
300
|
},
|
|
277
|
-
render
|
|
301
|
+
render: renderNow
|
|
278
302
|
};
|
|
279
303
|
|
|
280
|
-
|
|
304
|
+
renderNow();
|