@zvndev/darkmown 2.6.1-next.11.g82bb8d7 → 2.7.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 (73) hide show
  1. package/AGENTS.md +392 -77
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +316 -49
  4. package/docs/cli.md +51 -17
  5. package/docs/spec-alignment.md +25 -4
  6. package/grammar/wd-directives.gbnf +13 -5
  7. package/package.json +7 -3
  8. package/site/_/base.skin +1 -1
  9. package/site/_/footer.wd +1 -1
  10. package/site/pages/api-errors/index.wd +81 -0
  11. package/site/pages/app.wd +1 -1
  12. package/site/pages/attr-binding/index.wd +84 -0
  13. package/site/pages/blog/darkmown-2-7-0.md +176 -0
  14. package/site/pages/blog/darkmown-2-7-0.skin +127 -0
  15. package/site/pages/blog/no-unsafe-eval.md +66 -0
  16. package/site/pages/blog/no-unsafe-eval.skin +127 -0
  17. package/site/pages/blog/zero-js-by-default.md +1 -1
  18. package/site/pages/branch-state/index.wd +68 -0
  19. package/site/pages/docs/index.wd +320 -19
  20. package/site/pages/index.wd +3 -1
  21. package/site/pages/playground/index.js +63 -56
  22. package/site/pages/pulse/index.wd +1 -1
  23. package/site/pages/settings/index.wd +97 -0
  24. package/site/pages/showcase/index.wd +37 -1
  25. package/site/pages/upload/index.wd +63 -0
  26. package/site/pages/url-state/index.wd +70 -0
  27. package/src/builder.js +222 -34
  28. package/src/catalog.js +217 -21
  29. package/src/cli.js +6 -3
  30. package/src/compiler/actions.js +80 -12
  31. package/src/compiler/body.js +104 -28
  32. package/src/compiler/collections.js +29 -13
  33. package/src/compiler/context.js +9 -2
  34. package/src/compiler/expr-ast.js +14 -0
  35. package/src/compiler/fetch.js +8 -4
  36. package/src/compiler/forms.js +133 -14
  37. package/src/compiler/frontmatter.js +28 -7
  38. package/src/compiler/includes.js +46 -6
  39. package/src/compiler/interpolation.js +56 -1
  40. package/src/compiler/loops.js +489 -35
  41. package/src/compiler/markdown.js +457 -44
  42. package/src/compiler/page.js +8 -3
  43. package/src/compiler/predicates.js +84 -14
  44. package/src/compiler/state.js +167 -18
  45. package/src/compiler/structure.js +159 -29
  46. package/src/errors.js +111 -3
  47. package/src/feeds.js +140 -7
  48. package/src/grammar.js +53 -10
  49. package/src/runtime.js +509 -130
  50. package/src/runtime.min.js +2 -0
  51. package/src/runtime.min.js.map +1 -0
  52. package/src/statics.js +46 -2
  53. package/src/templates/blog/README.md +2 -0
  54. package/src/templates/dashboard/README.md +2 -0
  55. package/src/templates/landing/README.md +2 -0
  56. package/src/templates/starter/README.md +1 -1
  57. package/src/templates/store/README.md +2 -0
  58. package/src/tools/validate.js +14 -4
  59. package/types/builder.d.ts +6 -2
  60. package/types/catalog.d.ts +12 -0
  61. package/types/compiler/actions.d.ts +1 -1
  62. package/types/compiler/body.d.ts +0 -3
  63. package/types/compiler/forms.d.ts +12 -1
  64. package/types/compiler/frontmatter.d.ts +8 -1
  65. package/types/compiler/includes.d.ts +16 -2
  66. package/types/compiler/interpolation.d.ts +23 -0
  67. package/types/compiler/loops.d.ts +47 -0
  68. package/types/compiler/markdown.d.ts +63 -1
  69. package/types/compiler/predicates.d.ts +7 -1
  70. package/types/compiler/state.d.ts +8 -2
  71. package/types/compiler/structure.d.ts +32 -1
  72. package/types/feeds.d.ts +68 -2
  73. package/types/statics.d.ts +25 -0
@@ -26,6 +26,7 @@ Darkmown is a Markdown-native framework for mostly-static sites with tiny reacti
26
26
  - [Install](#install)
27
27
  - [The two formats](#the-two-formats)
28
28
  - [Routing rules](#routing-rules)
29
+ - [Directive lines](#directive-lines)
29
30
  - [Includes](#includes)
30
31
  - [Loops](#loops)
31
32
  - [Content collections](#content-collections)
@@ -38,21 +39,29 @@ Darkmown is a Markdown-native framework for mostly-static sites with tiny reacti
38
39
  - [Drafts](#drafts)
39
40
  - [Instant navigation](#instant-navigation)
40
41
  - [Images](#images)
42
+ - [Static assets](#static-assets)
41
43
  - [Sections](#sections)
42
44
  - [Reactive directives](#reactive-directives)
43
45
  - [Fetching data](#fetching-data)
44
46
  - [Global state — `:store`](#global-state--store)
47
+ - [URL as state](#url-as-state-from-url)
45
48
  - [Forms and persistence](#forms-and-persistence)
46
49
  - [Backends & deploy](#backends--deploy)
47
50
  - [Interactions](#interactions--slider-sortable-carousel)
48
51
  - [Theme toggle — `:theme`](#theme-toggle--theme)
49
52
  - [Scoped styles — `scoped`](#scoped-styles--scoped)
53
+ - [Inline attributes](#inline-attributes)
50
54
  - [Media directives](#media--video-audio-embed)
51
55
  - [Syntax highlighting](#syntax-highlighting)
52
- - [Trust boundary](#trust-boundary)
53
56
  - [Colocation](#colocation)
57
+ - [Programmatic compile](#programmatic-compile-compilefrommemory)
58
+ - [AI authoring](#ai-authoring)
54
59
  - [Compile error codes](#compile-error-codes)
55
60
  - [CLI reference](#cli-reference)
61
+ - [Editor support](#editor-support)
62
+ - [Accessibility](#accessibility)
63
+ - [Security](#security)
64
+ - [Limits](#limits)
56
65
  - [Spec status](#spec-status)
57
66
  </nav>
58
67
 
@@ -73,7 +82,7 @@ Or add Darkmown to an existing project with `npm install -D @zvndev/darkmown`. T
73
82
 
74
83
  Want to try the syntax before installing anything? The [playground](/playground/) compiles `.wd` and `.md` live in your browser — the same compiler the CLI ships — and shows the real `file:line` error when you break something.
75
84
 
76
- For editing, a [VS Code extension](https://github.com/ZVN-DEV/darkmown/tree/master/editors/vscode) gives `.wd` and `.skin` files syntax highlighting, snippets, and folding — install it from a `.vsix` built out of the framework repo (see its `editors/vscode` README; a Marketplace listing is pending).
85
+ For editing, there is a VS Code extension for `.wd` and `.skin` files: see [Editor support](#editor-support).
77
86
 
78
87
  `darkmown dev` rebuilds incrementally: it tracks which routes depend on each file (includes, colocated assets, loop data, collections), so editing one page or include recompiles only the routes it affects — and falls back to a full rebuild whenever a file is added, removed, or renamed. `routes.json`, the sitemap, and the RSS feed are regenerated on every rebuild, so they always describe the whole site.
79
88
 
@@ -89,6 +98,15 @@ For editing, a [VS Code extension](https://github.com/ZVN-DEV/darkmown/tree/mast
89
98
  - `.secret.wd`, `-draft.wd`, and hidden folders do not become pages.
90
99
  - `site/_/` is the include shelf, never a route.
91
100
 
101
+ ## Directive lines
102
+
103
+ A directive is a whole line, written flush against the left margin. Four rules cover what the compiler will and will not accept there.
104
+
105
+ - **One directive per line, unindented.** An indented `:state` (inside a list item, say) is prose, and a directive inside a fenced code block is code.
106
+ - **A bare keyword is an error.** A line that is exactly `:state`, `:button`, `@loop`, or any other directive that takes arguments used to render as literal text with no warning at all. It now reaches that directive's own handler and fails with that directive's coded message and `Use:` hint, so a lone `:state` gives `[WD201]` and a lone `:button` gives `[WD301]`. Three keywords stay valid bare, because they mean something bare: `:::` opens or closes a container, `:theme` declares the theme store, and `:carousel` opens a carousel with no autoplay.
107
+ - **To show a directive name as text, escape it.** Writing `\:fetch` renders the literal text `:fetch`. It is the ordinary CommonMark backslash escape, so it works anywhere Markdown does.
108
+ - **There is no comment syntax.** A trailing `#` or arrow note on a directive line is parsed as part of the value, not as a comment: `html: true (required)` seeds the string `true (required)`, which compiles clean while doing the opposite of what it looks like. Put explanations in the prose around the line.
109
+
92
110
  ## Includes
93
111
 
94
112
  - `@include /nav.wd` resolves from `site/_`.
@@ -207,6 +225,25 @@ A **missing** in-scope source is an empty list, not an error — `@loop meta.tag
207
225
 
208
226
  These clauses stay build-time when the source and every argument are static — a sorted, limited loop over a JSON file ships zero JS. The loop turns reactive only when the source or a clause reads `:state`/`:store`/`:fetch` data.
209
227
 
228
+ ### Table rows from a loop
229
+
230
+ A **static** loop whose body is bare `| … |` cells fills a Markdown table. Write the header in prose and let the loop supply the rows:
231
+
232
+ ```wd
233
+ | Item | Price |
234
+ | --- | --- |
235
+ @loop /products.json into row
236
+ | **{ row.name }** | { row.price } |
237
+ @endloop
238
+ ```
239
+
240
+ That renders one `<table>` with one `<tr>` per row, and each cell holds ordinary inline Markdown, links included. A format pipe works too, but its `|` has to be escaped inside a cell: `{ row.price \| money }`. Put the whole table inside the loop instead and you get a headerless table of the same shape; give the loop body its own `| --- |` separator and each row is a complete table, collapsed into one when the headers match.
241
+
242
+ Two limits to know before building on it:
243
+
244
+ - A pipe row written in prose **after** `@endloop` (a totals row) does not join the table. It stays a paragraph.
245
+ - A **reactive** loop over pipe rows is `[WD191]`, not silent breakage. A reactive row is cloned into a `<div>`, which is not a legal child of `<table>`, so there is no correct HTML to emit. Loop a static source for a Markdown table, or build reactive rows out of containers (`::: trow` / `::: td`) and style them with `display: table-row` / `table-cell`.
246
+
210
247
  ## Content collections
211
248
 
212
249
  Any folder under `site/pages/` is a **collection** — loop it by its bare name. No `content/` root, no marker file: a `site/pages/blog/` folder of posts *is* the `blog` collection. The whole [blog on this site](/blog/) is one such loop.
@@ -262,6 +299,31 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
262
299
  - If the name is declared `:state`, it becomes a live binding.
263
300
  - The page's frontmatter is in scope as `meta` — `{ meta.title }` prints a field.
264
301
  - Otherwise the text is left exactly as written — braces in prose never break a page or pull in the runtime.
302
+ - Values resolve **inside a link or image destination** too, and every brace in the destination resolves, not just one filling the whole value: `[Docs](/docs/{ region }/)` works.
303
+ - Values resolve in **raw HTML** as well (attributes and html blocks, HTML-escaped) on an `html: true` page, but those are painted once at build time rather than bound. Fenced code blocks and inline code spans are never rewritten, so a page can still show `` `[x](/p/{ p.slug }/)` `` as syntax.
304
+
305
+ ### Destinations that bind
306
+
307
+ A destination that reads `:state`, `:store`, `:computed`, a reactive loop row, or a loop meta variable is a **live binding**. The compiler emits the destination as a small template and the runtime rebuilds the value on every render:
308
+
309
+ ```wd
310
+ :state region = "eu"
311
+
312
+ :button "EU" -> region = "eu"
313
+ :button "US" -> region = "us"
314
+
315
+ [Open the docs for this region](/docs/{ region }/)
316
+ ```
317
+
318
+ Clicking a button rewrites the `href`. The build-time paint is still the seed (`/docs/eu/`), so the link works before the runtime loads and a crawler sees a real URL. Per-row destinations work the same way inside a reactive `@loop`: `- [{ p.name }](/products/{ p.slug }/)`.
319
+
320
+ Three rules worth knowing:
321
+
322
+ - **Values in URL position are percent-encoded**, so a `)`, a space, or an angle bracket coming from your data cannot end the link and hand the remainder back to the Markdown parser.
323
+ - **Dangerous schemes are refused twice.** The compiler vets what you wrote; the runtime re-checks what it assembled (control characters stripped first) and applies an **empty** attribute rather than a half-applied one when the value resolves to `javascript:`, `data:`, or `vbscript:`.
324
+ - **Raw HTML attributes do not bind.** `<a href="{ url }">` is painted once, with a build warning that names the position that does bind. Move the value into a Markdown destination, or drive the attribute from a colocated `.js` with `wd.subscribe`.
325
+
326
+ See it live on the [reactive links demo](/attr-binding/).
265
327
 
266
328
  ### Format pipes — `{ value | name:arg }`
267
329
 
@@ -300,6 +362,10 @@ site_url: https://example.com
300
362
 
301
363
  A page becomes an **RSS post** by carrying a `date:`; `excerpt:` is its feed summary (else `description:`, else — for a plain `.md` — its first paragraph). The sitemap's `<lastmod>` is the `date:` if set, else the file's git/last-modified date. `robots.txt` is always written; the `Sitemap:` line only appears with a `site_url`. Without `site_url`, robots still emits and the build prints a hint — it never breaks.
302
364
 
365
+ The feed carries the 20 newest posts by default. Set `rss_limit: 50` on the home page to change that. It takes digits only (a positive whole number of items), so `1e3`, `0x10`, and `7.0` are refused with `[WD950]` rather than silently coerced.
366
+
367
+ A sitemap over 50,000 URLs shards automatically into `sitemap-N.xml` files behind a `<sitemapindex>`, and a `date:` that is not a real `yyyy-mm-dd` is omitted from `<lastmod>` with a build hint instead of writing a broken value.
368
+
303
369
  ## Canonical URLs
304
370
 
305
371
  With `site_url` set, every page states its own absolute URL twice, as `<link rel="canonical">` and as `og:url`, both built from the **same route string** the sitemap and your internal links use, so nothing can drift out of agreement:
@@ -363,15 +429,51 @@ Add `transitions: true` to a page's frontmatter for **instant, flash-free naviga
363
429
 
364
430
  You don't size images by hand. Every `<img>` the compiler emits gets its intrinsic `width`/`height` read from the source file (so the page doesn't reflow as images decode), `decoding="async"`, and a priority split — the first image stays eager with `fetchpriority="high"`, the rest `loading="lazy"`. Anything you set yourself wins. The compiler measures, it doesn't resize, so keep source images web-sized.
365
431
 
432
+ ## Static assets
433
+
434
+ Non-page files live on the include shelf at `site/_/` and are copied into the build untouched:
435
+
436
+ - Any non-`.md`/`.wd` shelf file is served at `/__wd/media/<path>`, so `site/_/logo.svg` becomes `/__wd/media/logo.svg`. Reference it with a normal URL: `![logo](/__wd/media/logo.svg)`.
437
+ - Shelf `.json` files are served at `/__wd/data/<name>`, which is what `:fetch` reads. That is why `:fetch` works on any plain static host, with no server.
438
+ - A file colocated under `site/pages/` copies to `dist/` at its own path instead (see [Colocation](#colocation)), so `site/pages/logo.svg` is served at `/logo.svg`.
439
+
440
+ Use the shelf for assets shared across many pages, and colocate the ones a single page owns. Both ship with the right content-type.
441
+
366
442
  ## Sections
367
443
 
368
444
  `::: section #id .class` opens a container and `:::` closes it. Sections scope state: a `:state` declared inside a section belongs to that section, so two sections can both declare `count` without colliding. Bindings and buttons resolve to the nearest scope.
369
445
 
370
446
  A container named `nav` or `main` emits the real landmark element (`<nav class="nav">`, `<main class="main">`) so pages keep proper landmarks without raw HTML. Any other name stays a `<div>` with that class.
371
447
 
448
+ ### Accessibility attributes
449
+
450
+ Darkmown has no general attribute syntax on purpose: styling is `.class` tokens and behavior is `->` actions, which is what keeps output predictable and static pages script-free. That left one thing genuinely unreachable, the ARIA vocabulary a screen reader needs. Exactly three attribute names compile, on exactly two directives (`:::` and `:button`), always with a **double-quoted static value**:
451
+
452
+ ```wd
453
+ :state open = false
454
+
455
+ ::: card .note role="region" aria-label="Release notes" title="What changed"
456
+ Notes go here.
457
+ :::
458
+
459
+ ::: nav .menu role="navigation" aria-label="Main"
460
+ [Docs](/docs/)
461
+ :::
462
+
463
+ :button "Menu" aria-expanded="false" aria-controls="m" -> open toggle
464
+ ```
465
+
466
+ - On a container they interleave freely with `.class` and `#id` tokens and coexist with `.class when <predicate>`.
467
+ - On a `:button` they sit between the label and the `->`. An arrow inside a quoted value is safe: attributes are peeled before the action arrow is looked for.
468
+ - Values are HTML-escaped on emit, so nothing you write can close the attribute or open another.
469
+ - Anything outside the whitelist is `[WD650]`: `onclick=`, `style=`, `href=`, `class=`, `id=`, and `data-*` are refused, and so is `ARIA-LABEL` (the match is case-sensitive) or a single-quoted value. A whitelisted name with no double-quoted value is `[WD651]`.
470
+ - **Values are static text.** There is no `{ state }` interpolation inside an aria value in this release: `aria-label="{ who }"` emits the literal braces. For a live accessible name, write the element in raw HTML on an `html: true` page, or set it from a colocated `.js`.
471
+
472
+ Compile-time only: zero runtime bytes, and a static page carrying attributes stays `runtime: false`.
473
+
372
474
  ## Reactive directives
373
475
 
374
- Reactive pages opt into `/__wd/runtime.js` (~7.7 KB gzipped, under a CI-enforced 8 KB budget). Static pages do not.
476
+ Reactive pages opt into `/__wd/runtime.js`, a minified build of the readable `src/runtime.js` (~6.6 KB gzipped, under a CI-enforced 8 KB budget), with an external sourcemap beside it at `/__wd/runtime.js.map` carrying the original source, so DevTools shows real names and comments. Development serves the same bytes as production. Static pages ship none of it.
375
477
 
376
478
  ```wd
377
479
  :state count = 0
@@ -423,6 +525,8 @@ The aggregate's field is a bare row key (`price`, not `item.price`).
423
525
  :every 1s -> secs++
424
526
  ```
425
527
 
528
+ `:every` is a **page-level** registration. A reactive `@loop` compiles its body once into a template and the runtime clones it per row, so a timer inside that body would be registered once per row (three rows, three intervals) and a removed row's interval would keep firing. That placement is `[WD315]`. Declare the timer once outside the loop, at page level or inside the `::: section`, and act on the whole list (`:every 5s -> rows refetch`). It stays legal at page level, inside a `:::`, inside a **static** loop (N literal copies that never churn), and in a reactive loop's `@empty` branch. A `:button` inside a reactive loop is unaffected.
529
+
426
530
  ### Multi-branch conditionals
427
531
 
428
532
  A condition reads the same predicate grammar as `.class when` — a bare path (truthy), or the comparisons `==` `!=` `<` `<=` `>` `>=` `contains`, joined with `and`, `or`, and `not`. (`@loop … where` is the comparison-only subset.) Chain with `:else if` (any number; a bare `:else` must come last):
@@ -439,6 +543,8 @@ Free plan
439
543
 
440
544
  A whole chain compiles to nested conditional regions, so it folds at build time when every value is static and stays reactive otherwise — exactly like a single `:if`.
441
545
 
546
+ **Declarations inside a closed branch are live.** A `:state`, `:store`, or `:theme` written inside an `:if` branch that starts closed is hydrated the moment the branch opens, `persist` included. The seed is claimed once per **key**, not per node, so closing and re-opening the branch does not reset the value the reader set, and the claimed seed is that key's `reset` baseline. A `:computed` inside a closed branch stays dormant until the branch opens, by design. See the [closed-branch demo](/branch-state/).
547
+
442
548
  ### Reactive classes — `.class when …`
443
549
 
444
550
  A container class can react to state or the loop item. Static `.class` tokens stay as-is; add `when <predicate>` for a reactive one:
@@ -464,7 +570,7 @@ The predicate uses the `:if` whitelist (item fields, `:state`, numbers, strings,
464
570
  :effect q -> searches++
465
571
  ```
466
572
 
467
- Effects run after a render against settled state, never on the initial load, with a 10-pass settle cap guarding against loops.
573
+ Effects run after a render against settled state, never on the initial load, with a 10-pass settle cap guarding against loops. Like `:every`, an `:effect` is page-level: written inside a reactive `@loop` body it is `[WD315]`, because an effect watches a top-level state key and its actions target one, so there is no per-row meaning to give it.
468
574
 
469
575
  ### Button actions
470
576
 
@@ -482,11 +588,13 @@ Values are literals — a `"string"`, number, `true`/`false`/`null`, or inline J
482
588
  :button "Add to cart" -> cart.count++ ; cart.total += 9
483
589
  ```
484
590
 
591
+ A `:button` also accepts the three [accessibility attributes](#accessibility-attributes), written between the label and the `->`: `:button "Menu" aria-expanded="false" -> open toggle`.
592
+
485
593
  `list toggle v` and `list remove v` match by value, which is exact for strings and numbers but not for object members — remove row objects with the per-row `remove` action instead.
486
594
 
487
595
  ## Fetching data
488
596
 
489
- `:fetch name from "url"` declares state and fills it from JSON over the network. Shelf `.json` files are served at `/__wd/data/`. Each fetch auto-declares four states you can branch on: `name`, `name_loading`, `name_error`, and `name_empty`.
597
+ `:fetch name from "url"` declares state and fills it from JSON over the network. Shelf `.json` files are served at `/__wd/data/`. Each fetch auto-declares five states you can branch on: `name`, `name_loading`, `name_error`, `name_error_body`, and `name_empty`.
490
598
 
491
599
  ```wd
492
600
  :fetch team from "/__wd/data/team.json" timeout=8000 retry=2
@@ -508,6 +616,28 @@ The lifecycle regions announce themselves to assistive tech: a bare `:if name_lo
508
616
 
509
617
  Options after the URL: `method=` (default `GET`), `when=load|visible`, `timeout=<ms>`, `retry=<N>`, `headers=<key>`, and `body=<key>` (the last two name a `:state`/`:store` key). A URL can interpolate state (`"/api/users/{ userId }"`) and re-fetches automatically when that state changes; trigger a reload by hand with `:button "Reload" -> team refetch`. Loop a sub-path of fetched data with a dotted source — `@loop team.members into member`. URLs must be relative, `http(s)://`, or a leading `{ state }` interpolation — other schemes are rejected at compile time.
510
618
 
619
+ ### What the server said
620
+
621
+ A real API explains why it refused, and Darkmown surfaces that instead of a status line. When the failing response has a JSON body:
622
+
623
+ - **`name_error`** is the body's own `error` field, then its `message` field, falling back to `HTTP <status>` when the body carries neither or is not JSON at all.
624
+ - **`name_error_body`** is the whole parsed body (`null` when the response was not JSON), so per-field messages render without a line of your own JavaScript.
625
+
626
+ Given a `422` whose body is `{"error": "Pick a file first.", "fields": {"photo": "No file was attached."}}`:
627
+
628
+ ```wd
629
+ :fetch signup from "/api/signup" method=POST
630
+
631
+ :if signup_error
632
+ **{ signup_error }**
633
+ :if signup_error_body
634
+ Photo: { signup_error_body.fields.photo }
635
+ :endif
636
+ :endif
637
+ ```
638
+
639
+ The first line renders *Pick a file first.*, not *Error: HTTP 422*. Both keys are declared automatically by `:fetch` and by a round-trip `:form` (one with both `into` and `action=`), and both are cleared at the start of the next request, so a stale message never outlives the failure that produced it. See the [server errors demo](/api-errors/).
640
+
511
641
  ### Authenticated requests
512
642
 
513
643
  `headers=<key>` sends a state object as request headers; pair it with `:store` to persist a bearer token. Add `refresh="<url>"` and a `401` triggers a token refresh: Darkmown POSTs the current headers to the refresh URL, writes the renewed token back into the `headers` state, and retries the request once (concurrent `401`s share one refresh).
@@ -536,6 +666,44 @@ You have { cart } items.
536
666
  - On a multi-line seed, a `persist`/`ephemeral` token goes **after the closing bracket on the last line**, not on the declaration line.
537
667
  - `persist` means "survives a reload", `ephemeral` means "does not", and both are accepted on `:state`, `:store`, and `:theme`. The keyword only picks the default. `:store sidebarOpen = false ephemeral` is an in-memory store; `:state cart = [] persist` is a persisted one. Writing the token that matches the default is redundant but never wrong.
538
668
  - `:computed` takes neither: computed values are derived rather than stored, so persist the state they derive from instead (`WD211`).
669
+ - There is a third word, [`from-url`](#url-as-state-from-url), which says the value also lives in the query string. It composes with `persist` and belongs to `:state` only. Writing `persist` and `ephemeral` on one line (or the same word twice) is `[WD261]` rather than a token silently folded into the value.
670
+
671
+ ## URL as state: `from-url`
672
+
673
+ A filter nobody can link to is half a feature. Add `from-url` to a `:state` and the value lives in the query string as well as in memory: a reload keeps it, a shared link arrives with it applied, and the back button walks through it.
674
+
675
+ ```wd
676
+ :state q = "" from-url
677
+ :state tier = "all" from-url
678
+
679
+ :bind q placeholder="Search products"
680
+
681
+ :radio tier
682
+ - all
683
+ - budget
684
+ - premium
685
+
686
+ :state products = [{"name": "Aurora Lamp", "tier": "budget"}]
687
+
688
+ @loop products into p where p.name contains q
689
+ - **{ p.name }** ({ p.tier })
690
+ @empty
691
+ Nothing matches that search.
692
+ @endloop
693
+ ```
694
+
695
+ Type in the box and the address bar becomes `?q=aurora`. Reload, and the search comes back. Copy the URL into a new tab and it opens on the same view.
696
+
697
+ The rules:
698
+
699
+ - **The parameter is named after the state key.** A section-scoped key like `cart:items` becomes the parameter `cart.items`, so the name stays readable and stays unique.
700
+ - **A value equal to its declared seed drops its parameter,** so the default page keeps a clean URL.
701
+ - **Writes go through `history.replaceState`,** so filtering never fills the back button with one entry per keystroke. `popstate` re-reads on back and forward, and a parameter that is gone restores the seed.
702
+ - **It composes with `persist`,** and the boot precedence is URL, then stored value, then seed. A link somebody sent you beats what this browser remembers. When back navigation lands on a clean URL, the stored value follows the seed too.
703
+ - **Strings stay strings.** For any other seed type the parameter is JSON-parsed, falling back to the raw string.
704
+ - **`from-url` is `:state` only.** A `:store` is shared by every page and every tab while a query parameter belongs to one page's address, so `from-url` on a `:store` or a `:theme` is `[WD260]` rather than a guess. On a `:computed` it is `[WD211]`: derive the value from a state key that does come from the URL.
705
+
706
+ Live demo: [/url-state/](/url-state/).
539
707
 
540
708
  ## Forms and persistence
541
709
 
@@ -545,6 +713,57 @@ You have { cart } items.
545
713
  - `:if item.path` works inside reactive loops for per-row branches, and conditionals nest — an inner `:if` resolves after the outer branch, staying reactive.
546
714
  - Reactive pages expose `window.wd` (`get`, `set`, `subscribe`, `state`, `render`) so colocated `.js` can do anything directives can't. Section state is addressed as `sectionId:name`.
547
715
 
716
+ ### Bound controls outside a form
717
+
718
+ `:select`, `:radio`, and `:checkbox` mean two different things, and **where they sit decides which**. Inside a `:form` they are form fields, submitted by name (unchanged). Outside one they bind to a declared `:state` or `:store` of that name, exactly like `:bind` and `:slider`: move the control and the state changes, change the state and the control moves.
719
+
720
+ ```wd
721
+ :state density = "Comfortable"
722
+ :state previews = true
723
+
724
+ :select density
725
+ - Compact
726
+ - Comfortable
727
+ - Spacious
728
+
729
+ :checkbox previews
730
+ - Show image previews
731
+
732
+ :if density == "Compact"
733
+ Rows sit tight together.
734
+ :endif
735
+ ```
736
+
737
+ - **The state has to exist first.** A bound field naming state that is not declared is `[WD450]`, which spells out both readings: declare the state, or move the field inside a `:form`.
738
+ - **A bound `:checkbox` is a single boolean,** so it takes exactly one `- Label` line (the label shown beside it). Several options is `[WD451]`; for a set of choices use a `:radio` group. The multi-value checkbox group is the in-form behavior and is unchanged.
739
+ - **A bound `:radio` group keeps its shared `name`,** which is what makes the browser treat it as mutually exclusive, and carries the chosen option's text.
740
+
741
+ See the [settings demo](/settings/).
742
+
743
+ ### File upload
744
+
745
+ A `:form` that contains a file field posts **multipart**, so the file itself travels:
746
+
747
+ ```wd
748
+ :form into reply action="/api/upload/"
749
+ :input photo type=file required
750
+ :input caption placeholder="A caption (optional)"
751
+ :submit "Upload"
752
+ :endform
753
+
754
+ :if reply
755
+ Uploaded **{ reply.name }**, { reply.size } bytes.
756
+ :endif
757
+
758
+ :if reply_error
759
+ **{ reply_error }**
760
+ :endif
761
+ ```
762
+
763
+ The compiler writes `enctype="multipart/form-data"` for the browser's native submit, and the runtime sends real `FormData` with **no** content type of its own so the browser writes the boundary. A raw `<input type="file">` on an `html: true` page counts as a file field too.
764
+
765
+ Bound controls inside the form carry no `name`, so `FormData` would never see them; on the multipart path they are appended by their state key instead of being silently dropped. A file field on a `method="get"` form is `[WD452]`: a GET request has no body, so only the file's name would ever travel, which is the kind of failure that looks like it worked. See the [upload demo](/upload/).
766
+
548
767
  See it all live on the [Data & Forms page](/data/).
549
768
 
550
769
  ## Backends & deploy
@@ -559,12 +778,12 @@ Darkmown builds 100% static HTML — never per-request rendering. When you need
559
778
 
560
779
  `darkmown init shop --template store` scaffolds a cart **and** its `api/checkout.js`; `--template dashboard` ships a `:fetch` view **and** `api/metrics.js`.
561
780
 
562
- ## Interactions — `:slider`, `:sortable`, `:carousel`
781
+ ## Interactions — `:slider`, `sortable`, `:carousel`
563
782
 
564
- Pay-for-what-you-use: `:sortable`/`:carousel` compile to a tiny `/__wd/behaviors/*.js` module loaded only where used, budgeted separately from the ≤8 KB core runtime. `:slider` is compile-time only.
783
+ Pay-for-what-you-use: `sortable`/`:carousel` compile to a tiny `/__wd/behaviors/*.js` module loaded only where used, budgeted separately from the ≤8 KB core runtime. `:slider` is compile-time only.
565
784
 
566
785
  - `:slider name = v min=0 max=100 step=5` — a range input bound through `:bind`; range values coerce to Number.
567
- - `:sortable` (a `@loop` clause) — drag-to-reorder a `:state`/`:store` list via Pointer Events, with keyboard (Arrow Up/Down) + screen-reader announcements; valid on a plain reactive loop only.
786
+ - `sortable` (a bare `@loop` clause, written without a leading colon) — drag-to-reorder a `:state`/`:store` list via Pointer Events, with keyboard (Arrow Up/Down) + screen-reader announcements; valid on a plain reactive loop only.
568
787
  - `:carousel [autoplay=N]` — each direct child block is one slide (wrap each in its own block, e.g. `::: slide`); native scroll-snap plus prev/next, dots, and mouse drag. Autoplay respects `prefers-reduced-motion`.
569
788
 
570
789
  See all three live on the [Interactions page](/interactions/).
@@ -605,6 +824,16 @@ compiles to `.card[data-wd-scope="wd-7c21"] { … }`, and the component's markup
605
824
 
606
825
  Honest limits this release: only **whole-selector** `:global()` (no `.card :global(.x)` yet), and an **unused** scoped selector **warns but is not removed** (a colocated `.js` may add the class). See the side-by-side [Scoped demo](/scoped/).
607
826
 
827
+ ## Inline attributes
828
+
829
+ A trailing `{.class .class #id}` attaches classes or an id to the inline element directly before it, most often to style a link as a button without a wrapper:
830
+
831
+ ```wd
832
+ [Get started](/start/){.btn .lg}
833
+ ```
834
+
835
+ The block must follow the element with no space. It works on links, images, and emphasis, and it does **not** attach to inline code. It never collides with `{ name }` interpolation, because an interpolation always starts with a name, never a `.` or `#`. This page uses it: a link marked `{.no-prefetch}` is excluded from the `transitions: true` prerender hint.
836
+
608
837
  ## Media — `:video`, `:audio`, `:embed`
609
838
 
610
839
  Three compile-time-only directives (they emit no `data-wd-*`, so a media page stays zero-JS):
@@ -646,16 +875,6 @@ tokens dark
646
875
 
647
876
  The framework ships a small default `$code-*` set, so highlighting looks right out of the box. The stylesheet is **pay-for-what-you-use**: it's emitted and linked only on pages that actually contain a highlighted block — a page with no code ships nothing extra. A fence with an unknown or absent language renders as plain escaped `<code>` (no highlighting, no error); inline `` `code` `` is never highlighted, and there are deliberately no line numbers (they break copy-paste). See the live [Syntax highlighting demo](/highlight/).
648
877
 
649
- ## Trust boundary
650
-
651
- Darkmown is a trusted-author site generator: you compile content you wrote, the way you trust your own source. Three things define that boundary.
652
-
653
- - **Compile only content you authored.** Don't run `.md`/`.wd` you didn't write — user-generated content, third-party docs — through the compiler without sanitizing it first.
654
- - **Raw HTML is escaped by default.** Markdown renders with `html: false`, so a stray `<script>` in content becomes inert text — safe for multi-author collections out of the box. Set `html: true` in a page's frontmatter to pass your own raw HTML through verbatim; there is no built-in sanitizer, so keep it off pages with content you didn't write.
655
- - **`:fetch` and `:form action=` have no host allowlist.** Fetch/form URLs come straight from the page source — the compiler rejects non-http(s) schemes but does not restrict which hosts you call, so SSRF/exfiltration protection is the author's responsibility. Since 2.1 the runtime **interprets** validated `:computed` / `@loop … where` / `.class when` expressions from a compact AST instead of building a `new Function`, so **reactive pages need no `'unsafe-eval'`** — they run under the same strict, eval-free CSP as static pages.
656
-
657
- See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/master/SECURITY.md) in the package for the full security model.
658
-
659
878
  ## Colocation
660
879
 
661
880
  - A matching `.skin` file attaches CSS to the page (indentation-based, compiles to real CSS).
@@ -663,6 +882,21 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/master/SECURITY.md) i
663
882
  - Both work for included fragments too, by basename.
664
883
  - Any other file under `site/pages/` (an image, font, PDF — anything that isn't a `.md`/`.wd` route or a basename-matched `.skin`/`.js`) copies to `dist/` at its own path with the right content-type. So `site/pages/logo.svg` is served at `/logo.svg`. Assets below hidden path segments (`.`, `-`, or `_`) and symlinked page assets are skipped, matching the routing privacy rule. Use the `site/_/` shelf for assets shared across pages; colocate the ones a single page owns.
665
884
 
885
+ ## Programmatic compile: `compileFromMemory`
886
+
887
+ `darkmown build` reads from disk, but the compiler itself is filesystem-free. `compileFromMemory(files, entryPath, options)` compiles a page from an in-memory map of project-relative path to source, so the whole compile path bundles for the browser or any other non-Node host:
888
+
889
+ ```js
890
+ import { compileFromMemory } from "@zvndev/darkmown";
891
+
892
+ const { html, assets } = compileFromMemory(
893
+ { "site/pages/index.wd": "---\ntitle: Hi\n---\n\n:state n = 0\n" },
894
+ "site/pages/index.wd"
895
+ );
896
+ ```
897
+
898
+ Includes, colocated `.skin`/`.js` detection, and `@loop` JSON reads all resolve against the map (anything not in it is simply absent), and it throws the same `file:line` compile errors as the CLI. The [playground](/playground/) is built on exactly this entry point: markdown-it plus the compiler bundled into one asset, recompiling on every keystroke into an iframe. On disk, `compilePage(file, paths)` is the same compile with a filesystem reader injected for you.
899
+
666
900
  ## AI authoring
667
901
 
668
902
  Darkmown ships a machine-readable description of its own language, so an AI tool can learn `.wd` and be constrained to output that compiles. It is all generated from the compiler's own tables, so it never drifts from the real grammar.
@@ -707,7 +941,7 @@ Every author-facing compile error opens with a stable `WDxxx` code, so it is sea
707
941
 
708
942
  ```
709
943
  [WD201] Malformed :state in site/pages/index.wd:1: :state x.
710
- Use: :state name = value [persist] — e.g. :state count = 0
944
+ Use: :state name = value [persist|ephemeral] — e.g. :state count = 0
711
945
  ```
712
946
 
713
947
  The code is mirrored on the thrown error as `err.wd.code`, alongside `file`, `line`, `hint`, and a compilable `example`. Codes are grouped by subsystem — `WD0xx` source and frontmatter, `WD1xx` loops, `WD2xx` state and expressions, `WD3xx` actions, `WD4xx` forms, `WD5xx` fetching, `WD6xx` includes and structure, `WD7xx` media, `WD8xx` skins, `WD9xx` project and CLI.
@@ -726,6 +960,73 @@ The `darkmown` command (installed by `npx @zvndev/darkmown`) is the whole tool
726
960
  - `darkmown deploy <vercel|cloudflare> [--prod]` — build (target-aware) and deploy through the platform CLI, printing your URL; an unauthenticated CLI surfaces the login to run first.
727
961
  - `darkmown version` / `darkmown help` — print the installed version, or the full command summary and directive syntax.
728
962
 
963
+ ## Editor support
964
+
965
+ A [VS Code extension](https://github.com/ZVN-DEV/darkmown/tree/master/editors/vscode) gives `.wd` and `.skin` files syntax highlighting, snippets, and folding, so a `.wd` file reads as Markdown-plus-directives instead of broken Markdown. Build a `.vsix` from the framework repo and install it with `code --install-extension`; a Marketplace listing is pending.
966
+
967
+ ## Accessibility
968
+
969
+ Landmark and announcement basics are baked in at build time, so they cost zero JavaScript and a static page stays static:
970
+
971
+ - **Skip link.** The first focusable element on every page is a visually-hidden-until-focused "Skip to content" link. The shell guarantees it has a target: your own `<main>` is reused (an existing `id` wins, an id-less `<main>` gets `id="main"` stamped on), and a page without one is wrapped in `<main id="main">`. Restyle it through `.wd-skip-link` in a skin.
972
+ - **Document language.** `lang:` frontmatter sets `<html lang>` per page, defaulting to `en`.
973
+ - **Live `:fetch` regions.** A bare `:if name_loading` region compiles with `role="status" aria-live="polite"` and `:if name_error` with `role="alert"`, so screen readers announce the flips for free. A `role`/`aria-live` you write inside the region always wins.
974
+ - **Accessible names on controls.** `:input`, `:bind`, `:textarea`, `:select`, `:slider`, and the choice groups derive an `aria-label` from the placeholder, else a humanized field name, unless you supply one.
975
+ - **`role`, `aria-*`, and `title` on containers and buttons.** `::: card role="region" aria-label="Notes"` and `:button "Menu" aria-expanded="false" -> open toggle` compile without raw HTML. Values are static, double-quoted, and escaped on emit: see [Accessibility attributes](#accessibility-attributes).
976
+
977
+ There is still a ceiling, and it is stated in [Limits](#limits): those three names are the whole attribute vocabulary, and their values are static text, so a live accessible name needs raw HTML or a colocated `.js`.
978
+
979
+ ## Security
980
+
981
+ ### Trust boundary
982
+
983
+ Darkmown is a trusted-author site generator: you compile content you wrote, the way you trust your own source. Three things define that boundary.
984
+
985
+ - **Compile only content you authored.** Don't run `.md`/`.wd` you didn't write — user-generated content, third-party docs — through the compiler without sanitizing it first.
986
+ - **Raw HTML is escaped by default.** Markdown renders with `html: false`, so a stray `<script>` in content becomes inert text — safe for multi-author collections out of the box. Set `html: true` in a page's frontmatter to pass your own raw HTML through verbatim; there is no built-in sanitizer, so keep it off pages with content you didn't write.
987
+ - **`:fetch` and `:form action=` have no host allowlist.** Fetch/form URLs come straight from the page source — the compiler rejects non-http(s) schemes but does not restrict which hosts you call, so SSRF/exfiltration protection is the author's responsibility. Since 2.1 the runtime **interprets** validated `:computed` / `@loop … where` / `.class when` expressions from a compact AST instead of building a `new Function`, so **reactive pages need no `'unsafe-eval'`** — they run under the same strict, eval-free CSP as static pages.
988
+
989
+ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/master/SECURITY.md) in the package for the full security model.
990
+
991
+ ### Shipped security headers
992
+
993
+ Builds emit security response headers, so a deployed site gets sane defaults without a hand-written config. The build writes `dist/_headers` (Cloudflare Pages format), and the Vercel and local `serve` paths apply the equivalent:
994
+
995
+ - **`Content-Security-Policy`** with no `'unsafe-inline'` and no `'unsafe-eval'` on `script-src`, on every page. The one inline script the framework emits that CSP gates (the `transitions: true` speculationrules block) is authorized by a build-time `'sha256-…'` hash, and the inline state seed is a non-executable JSON data block CSP does not gate. Static and reactive pages get the same strict policy.
996
+ - **`X-Content-Type-Options: nosniff`**, a **`Referrer-Policy`**, and a **`frame-ancestors`** directive (clickjacking protection) on every page.
997
+ - A raw inline `<script>` you write into an `html: true` page is blocked by that policy. Put it in a colocated `.js` file (same-origin, allowed by `'self'`) or widen `script-src` deliberately.
998
+ - Calling another host with `:fetch` or `:form action=`? Widen `connect-src` (and `form-action` for a native form POST) in your deploy config. It is not derived from your page sources.
999
+ - The two embed origins (`youtube-nocookie.com`, `player.vimeo.com`) and `media-src 'self' https:` are pre-authorized, so `:embed` and remote media work out of the box.
1000
+
1001
+ ## Limits
1002
+
1003
+ Darkmown is small on purpose. Here is what it deliberately refuses and what it genuinely cannot do today, collected in one place so you can decide before you build rather than after.
1004
+
1005
+ **Refusals that will not change:**
1006
+
1007
+ - **`.md` never gets directives.** The extension is the feature gate, and it holds transitively: an `.md` page that includes a `.wd` still ships `runtime: false` and inert text.
1008
+ - **No `eval`, and a closed expression vocabulary.** Predicates, `:computed`, and directive actions are compile-time whitelists interpreted by a closed evaluator. There are no custom format pipes and no user-supplied functions anywhere in the language.
1009
+ - **No backend DSL.** `api/` is plain Web-standard JavaScript. Darkmown owns no server and never will.
1010
+ - **No built-in sanitizer.** The model is trusted-author; see [Security](#security).
1011
+
1012
+ **Structural limits, true today:**
1013
+
1014
+ - **No per-request rendering.** Everything is build time. A `:state`-seeded `@loop` *does* bake its rows into the initial HTML, so it is indexable, but anything behind `:fetch` is invisible to crawlers and to the sitemap. No personalization, no draft preview URLs.
1015
+ - **Auth is a public shell with `api/`-gated data.** A `:if session` gate ships *both* branches into the HTML, and a `:state`/`:store` seed is inlined into public HTML, so a token must never be written into a page. The workable architecture is a public page whose data is gated behind your `api/` endpoint.
1016
+ - **No layout or shell inheritance.** Every page includes its own nav and footer. There is no template a page extends.
1017
+ - **No arbitrary `<head>` content.** The document head is what frontmatter drives; you cannot inject your own tags.
1018
+ - **No route generation from data.** Collections are folders of files. A JSON array cannot become routes. `paginate N` is the one route multiplier, and only over an existing collection listing.
1019
+ - **Only accessibility attributes on containers and buttons.** `:::` and `:button` accept `role="…"`, `aria-…="…"`, and `title="…"`, and nothing else: `class=`, `id=`, `style=`, `data-*`, and event handlers are compile errors by design. Style with `.class` tokens, act with `->` actions, and reach for raw HTML on an `html: true` page when you genuinely need another attribute.
1020
+ - **Aria values are static text.** There is no `{ state }` interpolation inside `role`/`aria-*`/`title`, so a live accessible name means raw HTML or a colocated `.js`.
1021
+ - **Raw HTML attributes do not bind.** A Markdown link or image destination does ([Destinations that bind](#destinations-that-bind)), but `<a href="{ url }">` on an `html: true` page is painted once at build time and warned about. There is no element for the compiler to mark up.
1022
+ - **Reactive rows cannot fill a Markdown table.** A reactive row is cloned into a `<div>`, which is not a legal child of `<table>`, so `[WD191]` refuses it. Static loops fill tables fine; for reactive rows, build them from containers (`::: trow` / `::: td`) and style them as a table.
1023
+ - **No i18n beyond `lang:`.** No `hreflang`, no message catalog, no ambient locale, no locale routing. A multilingual site means duplicating the tree per locale.
1024
+ - **Every framework asset URL is absolute** (`/__wd/...`), so a Darkmown site cannot be mounted at a subpath. It is all-or-nothing per origin.
1025
+ - **Includes are macros, not components.** No children, no slots, no default arguments, and a missing argument renders `{ title }` literally rather than erroring.
1026
+ - **No `darkmown test`.** There is no built-in way to assert your own site's behavior. Use [`compileFromMemory`](#programmatic-compile-compilefrommemory) or a browser test runner.
1027
+
1028
+ **Migrating out is easy, and that is on purpose.** `dist/` is portable static HTML plus one runtime file of ~6.6 KB gzipped. No server to port, no proprietary component format, no framework baked into the output. Point any host at the folder, or hand it to whatever you move to next, and the site keeps working.
1029
+
729
1030
  ## Spec status
730
1031
 
731
1032
  The implementation is faithful to the original core thesis: Markdown-first authoring, no component ceremony, zero runtime on static pages, and tiny direct-DOM reactivity only when declared. `:store`, the full `:fetch` lifecycle (loading/error/empty states, dynamic URLs, `refetch`, dotted loop sources, authenticated `headers=` + `refresh=` token renewal), `:form` (including server round-trips), loop ergonomics (`sort by`/`limit`/`offset`/`reverse`/`@empty` and `$index`/`$number`/`$first`/`$last`/`$count`), `:if … :else if … :else`, reactive `.class when …`, `:effect`, `:computed`, and `persist` are all shipped and live — try them on the [Data & Forms page](/data/) and the [Reactive page](/reactive/). Still on the roadmap: HTML-fragment `swap` semantics and cart server sync. (The serverless `api/` model already shipped in 1.2.0 — see [Backends & deploy](#backends--deploy) above; a `.wd` backend DSL remains a deliberate non-goal.) See `docs/spec-alignment.md` in the package for the full audit.
@@ -162,7 +162,7 @@ The echo endpoint is only live on the deployed site: { reply_error }
162
162
  ## Three rules, no escape hatch
163
163
 
164
164
  - **Zero-JS static.** A page with no state ships no framework JavaScript. Open [the docs](/docs/) or [a plain `.md` page](/markdown/) and check the network tab.
165
- - **One runtime.** Every reactive page shares the same 7,654-byte gzipped runtime, CI-enforced under 8 KB. No per-component bundles, no hydration tax that grows with your UI. It runs under a strict CSP with no `unsafe-eval`.
165
+ - **One runtime.** Every reactive page shares the same 6,584-byte gzipped runtime, CI-enforced under 8 KB. No per-component bundles, no hydration tax that grows with your UI. It runs under a strict CSP with no `unsafe-eval`.
166
166
  - **One loop, one binding.** `@loop … into …` is the only loop. `{ name }` is the only interpolation. There is no second way to do either.
167
167
 
168
168
  ## A whole framework, in text
@@ -177,6 +177,8 @@ The echo endpoint is only live on the deployed site: { reply_error }
177
177
  ## See it for real
178
178
 
179
179
  :try "A whole live app in one file: fetch, loops, nested ifs, a form" href="/app/"
180
+ :try "A filter that lives in the URL, so the view is shareable" href="/url-state/"
181
+ :try "A link whose destination follows state as you click" href="/attr-binding/"
180
182
  :try "The docs, a .wd page built from includes" href="/docs/"
181
183
  :try "A plain .md page, where directives stay inert text" href="/markdown/"
182
184