@zvndev/darkmown 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +61 -6
  4. package/SECURITY.md +4 -4
  5. package/docs/cli.md +8 -0
  6. package/docs/spec-alignment.md +26 -6
  7. package/grammar/wd-directives.gbnf +58 -0
  8. package/package.json +5 -1
  9. package/site/_/footer.wd +1 -1
  10. package/site/_/nav.wd +1 -0
  11. package/site/pages/app.wd +1 -1
  12. package/site/pages/blog/zero-js-by-default.md +1 -1
  13. package/site/pages/docs/index.wd +19 -2
  14. package/site/pages/index.wd +5 -3
  15. package/site/pages/playground/index.js +158 -0
  16. package/site/pages/playground/index.skin +128 -0
  17. package/site/pages/playground/index.wd +51 -0
  18. package/site/pages/showcase/index.wd +1 -1
  19. package/src/builder.js +15 -8
  20. package/src/catalog.js +586 -0
  21. package/src/cli.js +8 -0
  22. package/src/compiler/actions.js +45 -12
  23. package/src/compiler/collections.js +16 -11
  24. package/src/compiler/context.js +52 -3
  25. package/src/compiler/expr-ast.js +296 -0
  26. package/src/compiler/fetch.js +12 -4
  27. package/src/compiler/forms.js +78 -24
  28. package/src/compiler/fs-reader.js +25 -0
  29. package/src/compiler/includes.js +20 -15
  30. package/src/compiler/index.js +1 -1
  31. package/src/compiler/loops.js +26 -8
  32. package/src/compiler/media.js +21 -7
  33. package/src/compiler/page.js +68 -15
  34. package/src/compiler/predicates.js +29 -18
  35. package/src/compiler/reader.js +60 -0
  36. package/src/compiler/state.js +81 -32
  37. package/src/compiler/structure.js +48 -17
  38. package/src/compiler.js +57 -6
  39. package/src/grammar.js +131 -0
  40. package/src/headers.js +15 -13
  41. package/src/index.js +2 -0
  42. package/src/runtime.js +77 -45
  43. package/src/statics.js +6 -7
  44. package/types/compiler.d.ts +13 -0
  45. package/types/index.d.ts +1 -0
@@ -0,0 +1,128 @@
1
+ tokens
2
+ pgink #eef2e6
3
+ pgbg #10160c
4
+ pgpanel #18210f
5
+ pgline #2c3a20
6
+ pgaccent #8fd694
7
+ pgmuted #9db089
8
+ pgradius 10px
9
+
10
+ page
11
+ margin 0
12
+ font system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
13
+ color $pgink
14
+ bg $pgbg
15
+ line-height 1.6
16
+
17
+ main
18
+ max-width 1100px
19
+ margin 0 auto
20
+ padding 2.5rem 1.5rem 4rem
21
+
22
+ .pg-intro h1
23
+ font-size 2.6rem
24
+ line-height 1.05
25
+ margin 0 0 .6rem
26
+ color $pgink
27
+
28
+ .pg-intro p
29
+ color $pgmuted
30
+ max-width 46rem
31
+
32
+ .pg-intro a
33
+ color $pgaccent
34
+
35
+ .pg-picker
36
+ display flex
37
+ flex-wrap wrap
38
+ gap .5rem
39
+ margin 1.4rem 0 1rem
40
+
41
+ .pg-picker button
42
+ font inherit
43
+ font-size .92rem
44
+ font-weight 560
45
+ padding .5rem 1rem
46
+ color $pgink
47
+ bg $pgpanel
48
+ border 1px solid $pgline
49
+ radius 999px
50
+ cursor pointer
51
+ transition background .14s ease, border-color .14s ease, transform .14s ease
52
+
53
+ .pg-picker button:hover
54
+ border-color $pgaccent
55
+ transform translateY(-1px)
56
+
57
+ .pg-picker button:focus-visible
58
+ outline 2px solid $pgaccent
59
+ outline-offset 2px
60
+
61
+ .pg-workbench
62
+ display grid
63
+ grid-template-columns 1fr 1fr
64
+ gap 1rem
65
+ margin-top 1.5rem
66
+ align-items stretch
67
+
68
+ .pg-pane
69
+ display flex
70
+ flex-direction column
71
+ min-width 0
72
+
73
+ .pg-label
74
+ font-size .78rem
75
+ font-weight 600
76
+ letter-spacing .04em
77
+ text-transform uppercase
78
+ color $pgmuted
79
+ margin-bottom .5rem
80
+
81
+ .pg-editor
82
+ width 100%
83
+ min-height 30rem
84
+ flex 1
85
+ resize vertical
86
+ padding 1rem 1.1rem
87
+ color $pgink
88
+ bg $pgpanel
89
+ border 1px solid $pgline
90
+ radius $pgradius
91
+ font ui-monospace, "SF Mono", Menlo, Consolas, monospace
92
+ font-size .9rem
93
+ line-height 1.55
94
+ tab-size 2
95
+
96
+ .pg-editor:focus-visible
97
+ outline none
98
+ border-color $pgaccent
99
+ box-shadow 0 0 0 3px rgba(143,214,148,.16)
100
+
101
+ .pg-preview
102
+ width 100%
103
+ min-height 30rem
104
+ flex 1
105
+ border 1px solid $pgline
106
+ radius $pgradius
107
+ bg #12190f
108
+
109
+ .pg-error
110
+ margin .75rem 0 0
111
+ padding .9rem 1.1rem
112
+ color #ffd9d0
113
+ bg #2a1712
114
+ border 1px solid #6b2c1e
115
+ radius $pgradius
116
+ font ui-monospace, "SF Mono", Menlo, Consolas, monospace
117
+ font-size .82rem
118
+ line-height 1.5
119
+ white-space pre-wrap
120
+ word-break break-word
121
+
122
+ @media (max-width: 820px)
123
+ .pg-workbench
124
+ grid-template-columns 1fr
125
+ .pg-editor
126
+ min-height 18rem
127
+ .pg-preview
128
+ min-height 22rem
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: Playground — run Markdown in your browser
3
+ description: Edit a .wd file and watch it compile and run in your browser — the same compiler the Darkmown CLI uses, no install required.
4
+ html: true
5
+ transitions: true
6
+ ---
7
+
8
+ @include /nav.wd
9
+
10
+ <main>
11
+
12
+ :state activeExample = "store"
13
+
14
+ ::: section .pg-intro
15
+
16
+ # Playground
17
+
18
+ Edit the Markdown on the left. It compiles **in your browser** — the exact same
19
+ compiler the CLI ships — and runs live on the right. No install, no build step.
20
+
21
+ ::: div .pg-picker
22
+ :button "Store + cart" -> activeExample = "store"
23
+ :button "Counter" -> activeExample = "counter"
24
+ :button "Plain Markdown" -> activeExample = "markdown"
25
+ :::
26
+
27
+ Now editing the **{ activeExample }** example. Change any text and the preview
28
+ re-renders as you type. Break the syntax on purpose — the real compiler error,
29
+ with `file:line` and a fix hint, shows up in the preview pane.
30
+
31
+ :::
32
+
33
+ ::: section .pg-workbench
34
+
35
+ ::: div .pg-pane .pg-pane-source
36
+ <label class="pg-label" for="pg-editor">Source</label>
37
+
38
+ <textarea id="pg-editor" class="pg-editor" spellcheck="false" autocapitalize="off" autocomplete="off" aria-label="Markdown source"></textarea>
39
+ :::
40
+
41
+ ::: div .pg-pane .pg-pane-preview
42
+ <label class="pg-label" for="pg-preview">Preview</label>
43
+
44
+ <iframe id="pg-preview" class="pg-preview" title="Live preview" sandbox="allow-scripts allow-same-origin"></iframe>
45
+
46
+ <pre id="pg-error" class="pg-error" role="alert" hidden></pre>
47
+ :::
48
+
49
+ :::
50
+
51
+ </main>
@@ -96,7 +96,7 @@ Darkmown's whole pitch is weight. A static page ships **zero** framework JavaScr
96
96
 
97
97
  ::: card .sc-scale
98
98
  - **Static Darkmown page** — `0 KB` framework JS. Forever.
99
- - **Reactive Darkmown page** — one shared `~7.5 KB` gzipped runtime, no per-component bundles.
99
+ - **Reactive Darkmown page** — one shared `~7.7 KB` gzipped runtime, no per-component bundles.
100
100
  - **A typical SPA** — ships a framework runtime in the **~15–60 KB** gzipped range *before* your app code, on every page (Alpine ~15 KB, Vue ~18 KB runtime, React 19 ~59 KB).
101
101
  :::
102
102
 
package/src/builder.js CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { discoverApiRoutes } from "./api-runner.js";
5
+ import { llmsText } from "./catalog.js";
5
6
  import { buildCollections } from "./compiler/collections.js";
6
7
  import { compilePage, parseFrontmatter } from "./compiler.js";
7
8
  import { createPaths, routesDir, shelfDir } from "./config.js";
@@ -352,9 +353,11 @@ function buildRoute(route, cwd, paths, collections, feedLink, warned) {
352
353
  * incremental builds keeps them consistent by construction.
353
354
  *
354
355
  * Cloudflare Pages reads dist/_headers for security headers (Vercel reads
355
- * vercel.json; the local server adds them in src/statics.js). Static routes get
356
- * the stricter CSP (no 'unsafe-eval'); reactive routes get the relaxed one. A
357
- * catch-all keeps assets and any future path covered.
356
+ * vercel.json; the local server adds them in src/statics.js). Since 2.1 both CSP
357
+ * variants are eval-free (the reactive runtime walks a validated AST, not
358
+ * `new Function`), so static and reactive routes resolve to the same policy; the
359
+ * per-route static override is kept as defense-in-depth. A catch-all keeps assets
360
+ * and any future path covered.
358
361
  * @param {RouteManifestEntry[]} manifest
359
362
  * @param {Paths} paths
360
363
  * @returns {void}
@@ -362,6 +365,10 @@ function buildRoute(route, cwd, paths, collections, feedLink, warned) {
362
365
  function emitGlobalFiles(manifest, paths) {
363
366
  fs.writeFileSync(path.join(paths.distRoot, "routes.json"), JSON.stringify(manifest, null, 2));
364
367
  fs.writeFileSync(path.join(paths.distRoot, "_headers"), renderCloudflareHeaders(manifest));
368
+ // `llms.txt` is the compact `.wd` cheatsheet an AI authoring tool fetches to
369
+ // prime a small model. It depends only on the framework version, so it re-emits
370
+ // identically on every build — cheap, and always present next to the site.
371
+ fs.writeFileSync(path.join(paths.distRoot, "llms.txt"), llmsText());
365
372
  }
366
373
 
367
374
  /**
@@ -924,10 +931,10 @@ export function stripRuntimeComments(source) {
924
931
  *
925
932
  * Cloudflare applies every matching block in order; when blocks set the same
926
933
  * header, the later block wins. So we emit the catch-all `/*` first (baseline
927
- * security headers + the relaxed reactive CSP that satisfies every page), then
928
- * one block per static route that overrides `Content-Security-Policy` with the
929
- * stricter, eval-free CSP. Reactive routes need no override — the catch-all
930
- * already carries the relaxed CSP.
934
+ * security headers + the reactive CSP, which since 2.1 is eval-free and satisfies
935
+ * every page), then one block per static route that re-states the same eval-free
936
+ * CSP as defense-in-depth. Reactive routes need no override — the catch-all
937
+ * already carries the eval-free CSP.
931
938
  *
932
939
  * Clean URLs mean a route like `/docs/` is served at both `/docs` and `/docs/`,
933
940
  * so each static route emits a path glob (`/docs`, `/docs/*`) that covers both.
@@ -941,7 +948,7 @@ export function renderCloudflareHeaders(manifest) {
941
948
  const blocks = [["/*", [...baseLines, ` Content-Security-Policy: ${REACTIVE_CSP}`]]];
942
949
 
943
950
  for (const entry of manifest) {
944
- if (entry.assets.runtime) continue; // reactive routes keep the catch-all (relaxed) CSP
951
+ if (entry.assets.runtime) continue; // reactive routes keep the catch-all (eval-free) CSP
945
952
  for (const pattern of cloudflarePathPatterns(entry.route)) {
946
953
  blocks.push([pattern, [` Content-Security-Policy: ${STATIC_CSP}`]]);
947
954
  }