kerfjs 1.0.2 → 2.0.1

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/CHANGELOG.md CHANGED
@@ -6,6 +6,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.0.1] - 2026-07-23
10
+
11
+
12
+
13
+ - Fixed a `morph()` bug where static text siblings of a fine-grained bound text hole were dropped after a structural re-render (e.g. `<div>{label} / static</div>` collapsing to just the label).
14
+
15
+
16
+ - New animated coding-session demo on the getting-started page: watch the canonical counter get typed out, served, clicked in a browser, and live-edited into a todo list.
17
+ - Added an animated architecture diagram and a link to the docs site from the README.
18
+
19
+ ## [2.0.0] - 2026-07-23
20
+
21
+
22
+
23
+ - `delegateCapture()` now uses `closest()`-style walk-up matching like `delegate()`, passing the matched ancestor to the handler; pass `{ match: 'direct' }` (new `DelegateOptions`) to restore exact-element matching.
24
+ - The dangerous-URL screen (`javascript:`, `vbscript:`, script-executing `data:` URLs) now throws an error in development instead of only warning. Production behavior is unchanged: warn and drop the attribute.
25
+
26
+
27
+ - New `kerfjs/html` tagged template: author kerf UIs with no build step (CDN / importmap, no JSX transform) with runtime semantics identical to JSX — signal holes become fine-grained bindings, attributes and text are escaped and URL-screened the same way.
28
+ - New no-build example app, **live-poll**, served exactly as authored — an importmap plus one `html`-templated module, with view-source showing the app.
29
+ - Added `<filter>` to the typed JSX intrinsic elements, so SVG filters compile in JSX-authored code.
30
+ - Fully-bound mounts are now a documented, test-pinned guarantee: a render that reads no signal `.value` runs exactly once, forever — every update is a direct per-node write.
31
+
32
+
33
+ - Controlled form state now survives user interaction: `checked`, `value`, and `selected` DOM properties are synced when the reconciler mutates those attributes, so a clicked checkbox or typed-into input no longer ignores later updates.
34
+ - Fixed stale fine-grained bindings after `arraySignal.update()`: in-place row updates now re-wire bindings whose signal instance changed, instead of leaving effects reading the old row object forever.
35
+
36
+
37
+ - New runtime dev-mode override: set `globalThis.KERF_DEV = false` (or `true`) to control dev mode without a bundler — CDN/importmap apps are no longer stuck in dev mode in production.
38
+ - Two new opt-in dev warnings: `KERF_DEV_WARN_STALE_BINDING` flags bindings that silently go stale on the byte-equal fast path, and `KERF_DEV_WARN_VALUE_ONLY_RERENDER` flags re-renders whose only changes could have been fine-grained bindings.
39
+ - `defineStore`'s dev-mode `get()` snapshot now returns a deep read-only proxy instead of freezing the live state: nested mutations like `get().nested.x = 1` are caught too, and they throw a descriptive `TypeError` rather than failing silently.
40
+
41
+
42
+ - Docs repositioned around the "values bind, structure re-renders" idiom as the primary way to render dynamic values, across the overview, reactivity guide, and AI assistant configs.
43
+ - New guide covering the no-build authoring path and example app, plus a documentation-wide accuracy pass (delegate capture semantics, `effect()` cleanup returns, URL-screen behavior, and more).
44
+
9
45
  ## [1.0.2] - 2026-07-22
10
46
 
11
47
 
package/LICENSE CHANGED
@@ -25,7 +25,7 @@ SOFTWARE.
25
25
 
26
26
  ## Acknowledgements
27
27
 
28
- `src/diff.ts` re-implements the DOM-reconciliation algorithm of
28
+ `src/morph.ts` re-implements the DOM-reconciliation algorithm of
29
29
  [morphdom](https://github.com/patrick-steele-idem/morphdom) by Patrick
30
30
  Steele-Idem, which is also distributed under the MIT License:
31
31
 
@@ -51,3 +51,31 @@ Steele-Idem, which is also distributed under the MIT License:
51
51
  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
52
52
  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
53
53
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
54
+
55
+ ---
56
+
57
+ `src/attrSelector.ts`'s `cssEscapeIdent` adapts the CSS identifier escaping of
58
+ the [CSS.escape polyfill](https://github.com/mathiasbynens/CSS.escape) by
59
+ Mathias Bynens, which is also distributed under the MIT License:
60
+
61
+ Copyright Mathias Bynens <https://mathiasbynens.be/>
62
+
63
+ Permission is hereby granted, free of charge, to any person obtaining
64
+ a copy of this software and associated documentation files (the
65
+ "Software"), to deal in the Software without restriction, including
66
+ without limitation the rights to use, copy, modify, merge, publish,
67
+ distribute, sublicense, and/or sell copies of the Software, and to
68
+ permit persons to whom the Software is furnished to do so, subject to
69
+ the following conditions:
70
+
71
+ The above copyright notice and this permission notice shall be
72
+ included in all copies or substantial portions of the Software.
73
+
74
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
75
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
76
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
77
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
78
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
79
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
80
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
81
+ SOFTWARE.
package/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
 
7
7
  <p align="center"><em>The smallest cut.</em></p>
8
8
 
9
+ <p align="center">
10
+ <a href="https://brianwestphal.github.io/kerf/"><strong>brianwestphal.github.io/kerf</strong></a> — docs · examples · live demo
11
+ </p>
12
+
9
13
  ---
10
14
 
11
15
  > Introducing Kerf.
@@ -29,23 +33,27 @@ mount(document.getElementById('app')!, () => (
29
33
 
30
34
  That's it. Your JSX renders to HTML strings, kerf's native diff applies the minimum DOM mutations to make the live tree match, and signals re-run the render only when something they read actually changed.
31
35
 
36
+ Here's the whole development loop — write a component, run the dev server, click around, edit, watch the browser pick it up:
37
+
38
+ [![Animated coding session: a counter component is typed line by line into an editor, npm run dev starts in a terminal and the localhost link is clicked, the running app is clicked in a browser, then back in the editor a computed class is added — selecting "btn" and typing a bound {cls} hole — and the browser shows the button change color at the fifth click](https://brianwestphal.github.io/kerf/demos/getting-started.svg)](https://brianwestphal.github.io/kerf/getting-started/)
39
+
32
40
  ## Why Kerf
33
41
 
34
42
  1. **Small bundle.** ~11 KB minified + gzipped including `@preact/signals-core` (~12 KB with `arraySignal`). One runtime dependency. No virtual DOM, no scheduler, no concurrent-mode machinery. On the official [krausest js-framework-benchmark](https://krausest.github.io/js-framework-benchmark/current.html) — where kerf is a listed entry, measured on the same reference machine as every competitor ([local mirror](./bench/results.md)) — kerf is in the same cluster as Vue, vanjs, and Lit on most operations; Solid's compiler leads the update-path benchmarks (notably `partial update`), which kerf doesn't try to match by design — no compiler.
35
43
 
36
44
  2. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it *is* the DOM.
37
45
 
38
- 3. **Fine-grained updates, opt-in.** Hand a signal *itself* into a JSX hole — `class={selectedId}` or `{status}` — and kerf binds that one node directly: when the signal changes, only that attribute or text node updates, with no render re-run and no list reconcile. It's a surgical tier beneath the coarse render effect, for the *external-state-drives-one-spot* pattern — a selection flip on a 10,000-row table touches exactly one class.
46
+ 3. **Values bind, structure re-renders.** Hand a signal *itself* into a JSX hole — `class={selectedId}` or `{status}` — and kerf binds that one node directly: when the signal changes, only that attribute or text node updates, with no render re-run and no list reconcile. A selection flip on a 10,000-row table touches exactly one class. Taken to its logical end: a mount whose render reads no `.value` at all runs **exactly once, forever** — every subsequent update flows through the per-hole bindings. Read `.value` in the render only when the *structure* depends on it (conditionals, list shape).
39
47
 
40
48
  4. **Focus, selection, listeners survive re-renders — even mid-list.** The reconciler morphs instead of rebuilding, so caret position, selection range, IME composition, and delegated listeners survive every re-render. Keyed lists get the same treatment: same-identity rows are updated *in place* rather than recreated, so a row reorder or a single-cell edit no longer blows away focus, scroll, or an in-flight animation the way node replacement does.
41
49
 
42
- 5. **Safe by default.** Text and attribute values are HTML-escaped automatically, URL attributes are scheme-screened (`javascript:` / script-carrying `data:` dropped), inline `on*` handlers are rejected outright, and the same screening covers the fine-grained bound path — so untrusted data stays inert even when kerf is dropped into someone else's page. `raw()` is the explicit, auditable opt-out.
50
+ 5. **Safe by default.** Text and attribute values are HTML-escaped automatically, URL attributes are scheme-screened (`javascript:` / script-carrying `data:` dropped), inline `on*` handlers are rejected outright, and the same screening covers the fine-grained bound path — so untrusted data stays inert even when kerf is dropped into someone else's page. The URL screen fails loudly at your desk (throws in development) and degrades safely in the field (warns and drops in production). `raw()` is the explicit, auditable opt-out.
43
51
 
44
- 6. **Small public API.** ~17 exports from the main barrel (plus `arraySignal` on its own subpath). No hooks, no lifecycle, no per-instance state. Components are plain functions that return JSX.
52
+ 6. **Small public API.** ~17 exports from the main barrel (plus `arraySignal` and the `html` tagged template on their own subpaths). No hooks, no lifecycle, no per-instance state. Components are plain functions that return JSX.
45
53
 
46
- 7. **Plain TS, plain JSX, plain ESM.** Drops into anything using esbuild / Vite / tsup. No plugin chain.
54
+ 7. **Plain TS, plain JSX, plain ESM.** Drops into anything using esbuild / Vite / tsup. No plugin chain. And with the `html` tagged template (`import { html } from 'kerfjs/html'` — identical runtime semantics to JSX), a CDN / importmap project needs no build step at all.
47
55
 
48
- 8. **Grown-up tooling around a tiny core.** An [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) that enforces the hard rules at edit time, a `create-kerf-component` scaffold for publishable component packages, drop-in AI-assistant configs, and side-by-side migration guides for a dozen-plus frameworks — none of which grows the core runtime past ~11 KB.
56
+ 8. **Grown-up tooling around a tiny core.** An [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) that enforces the hard rules at edit time, an opt-in family of `KERF_DEV_WARN_*` runtime warnings that catch the classic mistakes in development (with zero production cost), a `create-kerf-component` scaffold for publishable component packages, drop-in AI-assistant configs, and side-by-side migration guides for a dozen-plus frameworks — none of which grows the core runtime past ~11 KB.
49
57
 
50
58
  ## When to use Kerf
51
59
 
@@ -91,16 +99,12 @@ mount(root, () => (
91
99
  <div>
92
100
  <h1>Cart ({cart.state.value.items.length})</h1>
93
101
  <ul>
94
- {each(
95
- cart.state.value.items,
96
- (item) => (
97
- <li>
98
- {item.name}
99
- <button data-action="remove" data-id={item.id}>×</button>
100
- </li>
101
- ),
102
- (item) => item.id,
103
- )}
102
+ {each(cart.state.value.items, (item) => (
103
+ <li data-key={item.id}>
104
+ {item.name}
105
+ <button data-action="remove" data-id={item.id}>×</button>
106
+ </li>
107
+ ))}
104
108
  </ul>
105
109
  <p>Doubled count: {doubled.value}</p>
106
110
  </div>
@@ -112,6 +116,17 @@ delegate(root, 'click', '[data-action="remove"]', (_e, btn) => {
112
116
  });
113
117
  ```
114
118
 
119
+ The stringly-typed `'[data-action="remove"]'` pair above can be made rename-safe with the `attr()` helper — declare the attribute once and use it on both sides:
120
+
121
+ ```ts
122
+ import { attr } from 'kerfjs';
123
+
124
+ const REMOVE = attr('data-action', 'remove'); // pre-escaped name/value/selector
125
+
126
+ <button {...REMOVE.attrs} data-id={item.id}>×</button>; // in JSX
127
+ delegate(root, 'click', REMOVE.selector, (_e, btn) => { /* … */ }); // in delegation
128
+ ```
129
+
115
130
  ### Fine-grained updates: bind a signal into a hole
116
131
 
117
132
  Inside a `mount()`, hand a signal *itself* (not its `.value`) into an attribute or text position and kerf wires that hole straight to the signal — the render function never re-runs and the list reconciler never walks:
@@ -128,7 +143,9 @@ mount(root, () => (
128
143
  status.value = 'saving'; // updates the class + the text node directly — no re-render
129
144
  ```
130
145
 
131
- The headline use is external state driving one spot: a `selectedId` flipping a single row's class inside a 10,000-row `each()` list touches exactly that one node, no reconcile. Works in static content and inside `each()` rows (a row's binding is torn down with the row); outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal just snapshots its current value. See [`docs/2-reactivity.md`](./docs/2-reactivity.md) §2.9.
146
+ The headline use is external state driving one spot: a `selectedId` flipping a single row's class inside a 10,000-row `each()` list touches exactly that one node, no reconcile. Works in static content and inside `each()` rows (a row's binding is torn down with the row); outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal just snapshots its current value.
147
+
148
+ This is kerf's guiding idiom — *values bind, structure re-renders*: pass the signal itself wherever a hole is just a value, and read `.value` in the render function only where the JSX structure depends on it. A render that reads no `.value` runs exactly once; from then on every update is a direct write to the node it concerns. See [`docs/2-reactivity.md`](./docs/2-reactivity.md) §2.9.
132
149
 
133
150
  ### Long keyed lists: `arraySignal`
134
151
 
@@ -164,6 +181,25 @@ morph(liveCard, raw(htmlFromServer)); // SafeHtml
164
181
 
165
182
  Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip-children`, `data-morph-preserve`, focused-input value + selection preservation, the `<details>` / `<dialog>` user-agent-owned `open` rule all carry over. Use it for SSR-fragment hydration, page-refresh diffs, third-party widget remounts. See [`docs/4-render.md`](./docs/4-render.md) §4.4.3.
166
183
 
184
+ ### No build step at all: the `html` tagged template
185
+
186
+ "No compiler" isn't just a JSX story. The `html` tagged template from `kerfjs/html` has **identical runtime semantics to JSX** — escaping, boolean/nullish attribute rules, URL screening, `on*` rejection, fine-grained signal bindings, `each()` composition — with no transform, so a plain `<script type="module">` on a CDN / importmap page is a complete kerf app:
187
+
188
+ ```html
189
+ <script type="module">
190
+ import { signal, mount, each } from 'https://esm.sh/kerfjs';
191
+ import { html } from 'https://esm.sh/kerfjs/html';
192
+
193
+ const items = signal([{ id: 1, label: 'no build step' }]);
194
+
195
+ mount(document.getElementById('app'), () => html`
196
+ <ul>${each(items.value, (i) => html`<li id="${i.id}">${i.label}</li>`)}</ul>
197
+ `);
198
+ </script>
199
+ ```
200
+
201
+ Attribute names are written verbatim (`class`, not `className`), and holes are only legal in text positions or as a complete attribute value — anything ambiguous throws with an actionable message. Static template parts parse once per call site. See [`docs/6-jsx-runtime.md`](./docs/6-jsx-runtime.md) §6.11 — or the [live-poll example](https://brianwestphal.github.io/kerf/examples/complete/live-poll/), a complete app served exactly as authored: no bundler ever touches it.
202
+
167
203
  ## Install
168
204
 
169
205
  ```bash
@@ -225,7 +261,7 @@ A *kerf* is the narrow strip of material a saw blade removes when cutting — th
225
261
 
226
262
  ## Status
227
263
 
228
- 1.0 — the public API is stable and follows semver from here. See [CHANGELOG.md](./CHANGELOG.md) for the current version and what's shipped.
264
+ Stable — the public API follows semver. See [CHANGELOG.md](./CHANGELOG.md) for the current version and what's shipped.
229
265
 
230
266
  ## Sponsor
231
267
 
package/ai/cursorrules CHANGED
@@ -1,4 +1,4 @@
1
- <!-- kerf-skill-version: 1.3.0 -->
1
+ <!-- kerf-skill-version: 1.8.0 -->
2
2
  # kerf.cursorrules — rules for building apps with kerf
3
3
  #
4
4
  # Drop this file into your project as `.cursorrules` (Cursor will pick it
@@ -42,12 +42,13 @@ import { arraySignal } from 'kerfjs/array-signal';
42
42
  | `morph(liveRoot, template)` | one-shot reconcile against an already-populated element (SSR hydration, page-refresh diffs). Template can be `Element`, `SafeHtml`, or HTML string |
43
43
  | `each(items, render, cacheKey?)` | keyed list iteration; per-row memoization on object identity (+ optional cacheKey — a passive comparator for external state). Distinct from `data-key` on the rendered element |
44
44
  | `delegate(root, type, sel, h)` | one listener at the root, walks `closest(selector)` from target |
45
- | `delegateCapture(root, type, sel, h)` | capture-phase escape hatch, strict `target.matches()` |
45
+ | `delegateCapture(root, type, sel, h, opts?)` | capture-phase escape hatch; `closest()` walk-up by default (same as `delegate`); pass `{ match: 'direct' }` for strict `target.matches()` |
46
46
  | `attr(name, value)` | pre-computed `AttrSpec<N,V>` — `.selector` for `delegate()`, `.attrs` to spread into JSX (rename-safe) |
47
47
  | `attr(name)` | dynamic factory — `attr<N,V=string>(name)` returns `(value: V) => { readonly [name]: V }`; both generics off → N inferred, V defaults to string; specify both to constrain values |
48
48
  | `toElement(jsx)` | parse JSX into a DOM node (SVG-aware). Single-root → `Element`; multi-root (`<><svg/> label</>`, two icons side by side) → `DocumentFragment` that `appendChild`/`replaceChildren`/`append` inlines into the parent. |
49
49
  | `raw(html)` | inject pre-escaped HTML |
50
50
  | `arraySignal(initial?)` | granular keyed-list signal at `kerfjs/array-signal` subpath; `each()` reconciles in O(patches) |
51
+ | `` html`…` `` | tagged template at `kerfjs/html` subpath — JSX-identical runtime semantics with NO build step (CDN/importmap projects). Real HTML attribute names (`class`, not `className`); holes only in text positions or as a COMPLETE attribute value (`attr=${v}` / `attr="${v}"`) |
51
52
 
52
53
  ## Hard rules — get these right on the first try
53
54
 
@@ -58,7 +59,7 @@ import { arraySignal } from 'kerfjs/array-signal';
58
59
  5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
59
60
  6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX.
60
61
  7. **No `<MyComponent />` semantics with hooks.** Components are plain functions returning JSX. State lives in module-scope signals or stores, never in component closures.
61
- 8. **Signal reads must happen INSIDE the render function** to be tracked. `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Move the read inside.
62
+ 8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX *structure* depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Bind a STABLE signal/computed instance per hole (a `computed` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (`KERF_DEV_WARN_STALE_BINDING=1` detects it). Endpoint: a render reading NO `.value` runs exactly once — a fully bound mount never re-renders; `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders that could have been bindings.
62
63
  9. **Store actions receive `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
63
64
  10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root instead.
64
65
  11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise (single-row edits, append-to-end). For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
@@ -73,7 +74,7 @@ When deciding which primitive to reach for, work down the axes:
73
74
  **Events.**
74
75
  - Originates inside the mount tree → `delegate(rootEl, type, sel, handler)`. Originates outside (window-level keyboard, online/offline, beforeunload) → native `window.addEventListener` at module top-level.
75
76
  - Gesture that needs to follow an element after press (drag, draw, resize) → at the start event, `el.setPointerCapture(e.pointerId)`. Subsequent `pointermove` / `pointerup` redirect to the captured element and `delegate(rootEl, 'pointermove', '[data-card]', …)` still picks them up. Don't reach for `window.addEventListener` for in-mount-tree gestures.
76
- - Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or strict element-match → `delegateCapture()`.
77
+ - Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or capture-phase interception → `delegateCapture()` (also `closest()`-matched by default). Need strict element-match? Add `{ match: 'direct' }` on either helper.
77
78
 
78
79
  **Lists.**
79
80
  - Items change across renders (todos, chat messages, table rows) → `each(items, render)`.
@@ -89,18 +90,22 @@ When deciding which primitive to reach for, work down the axes:
89
90
  **Raw HTML.**
90
91
  - User-controlled HTML → sanitize first (DOMPurify) then `raw(sanitized)`.
91
92
  - Author-controlled trusted HTML → `raw(html)` directly.
93
+ - Dangerous URLs (`javascript:`/`vbscript:`/script-executing `data:`) on `href`/`src`/`xlink:href`/`formaction`/`action`/`data` are dropped — kerf THROWS in dev, WARNS + drops in prod. Sanitize user URLs upstream; wrap an intentional trusted one in `raw(url)` to bypass the screen in both modes.
92
94
 
93
95
  ## Canonical patterns
94
96
 
95
97
  ```tsx
96
- // Signal + mount
98
+ // Signal + mount. THE core idiom — values bind, structure re-renders:
99
+ // pass the signal ITSELF into a value hole ({count}, class={sig}) so kerf
100
+ // updates that one node directly with no render re-run; read `.value` only
101
+ // when the JSX STRUCTURE depends on the signal (conditionals, list shape).
97
102
  const count = signal(0);
98
103
  const ACTIONS = { inc: attr('data-action', 'inc') } as const satisfies Record<string, AttrSpec<'data-action'>>;
99
104
 
100
105
  mount(document.getElementById('app')!, () => (
101
106
  <div>
102
107
  <button {...ACTIONS.inc.attrs}>+</button>
103
- <span>{count.value}</span>
108
+ <span>{count}</span>
104
109
  </div>
105
110
  ));
106
111
  delegate(rootEl, 'click', ACTIONS.inc.selector, () => { count.value += 1; });
@@ -124,6 +129,15 @@ const cart = defineStore({
124
129
  // One-shot reconcile against existing DOM (no signals)
125
130
  morph(liveCard, '<article class="card">…</article>');
126
131
 
132
+ // No build step (CDN / importmap): the html tagged template instead of JSX.
133
+ // Same runtime semantics as JSX; write real HTML attribute names; a hole must
134
+ // be a text position or a COMPLETE attribute value (partial values throw).
135
+ import { html } from 'kerfjs/html';
136
+ mount(rootEl, () => html`
137
+ <div class="${cls}">Count: ${count}</div>
138
+ <ul>${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}</ul>
139
+ `);
140
+
127
141
  // Fine-grained binding (opt-in): pass the signal/computed ITSELF into a hole
128
142
  // so a change updates ONLY that node (no render re-run, no reconcile). For a
129
143
  // hot spot driven by an external signal (selection class) — not everywhere.
@@ -150,6 +164,7 @@ mount(listEl, () => (
150
164
  - Drag/drop / state change has no visible effect, only stuff *outside* `each()` updates → you used `each(STATIC_ARRAY, …)` whose row render reads signals. Replace the outer with `STATIC_ARRAY.map(...)`; keep inner `each()` for the dynamic sub-list. See Hard Rule 14.
151
165
  - Row-enter CSS animation no longer replays when only a row's *content* changed (kerf ≥ 0.15.0) → 0.15.0+ morphs a same-identity, same-position row *in place* instead of recreating its node, so a mount-keyed `@keyframes` never re-triggers on a content-only update (≤ 0.14.x recreated the node, so it fired; the intentional flip side is that focus, scroll, IME, and in-progress transitions now survive). Key the animation on a state-class toggle, not element creation; to force a remount, churn the row's identity (new object ref / `data-key`).
152
166
  - Want a hot spot to update without re-running the whole render → fine-grained binding: pass the signal/`computed` ITSELF into the attr/text hole (`class={computed(() => …)}`), not `.value`. Use `computed()` not a bare `() => …` (memoization keeps a shared-signal flip to ~O(changed nodes)). Opt-in per hole. Limit: a bound hole depending on the row's OWN mutated data goes stale on a granular in-place update — use plain interpolation there.
167
+ - `` html`` ``: partial attribute values are not supported → in `kerfjs/html` templates a hole must be the COMPLETE attribute value. Replace `class="a ${b}"` with a pre-built string (`` class="${`a ${b}`}" ``) or, for a bound attribute, `class="${computed(() => `a ${b.value}`)}"`.
153
168
 
154
169
  ## Server / SSR
155
170
 
package/ai/manifest.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
- "kerfjsVersion": "1.0.2",
2
+ "kerfjsVersion": "2.0.1",
3
3
  "files": [
4
4
  {
5
5
  "name": "skill",
6
6
  "source": "kerf.claude-skill.md",
7
7
  "bundle": "ai/skill.md",
8
8
  "dest": ".claude/skills/kerf-app/SKILL.md",
9
- "version": "1.3.0",
10
- "sha256": "789cfd9bbe0fa8a01b779595912b7fef569b3c021be412d35f453476e576bac6"
9
+ "version": "1.8.0",
10
+ "sha256": "76b8a88bc7aaf9d50e6f3eee7b57a976b3279127f5d4970599aef1f839a06655"
11
11
  },
12
12
  {
13
13
  "name": "cursorrules",
14
14
  "source": "kerf.cursorrules",
15
15
  "bundle": "ai/cursorrules",
16
16
  "dest": ".cursorrules",
17
- "version": "1.3.0",
18
- "sha256": "2524a42cc3b488f8cc3cea736cf5b8d3e57d868c85260342ec50905243aa65f1"
17
+ "version": "1.8.0",
18
+ "sha256": "f91402df009941b4198ea659498f458d2433843d47a3b524b0ca5e2d3d2fc949"
19
19
  }
20
20
  ]
21
21
  }
package/ai/skill.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: kerf-app
3
3
  description: Build UIs in the kerf reactive framework (https://github.com/brianwestphal/kerf). Use this skill whenever the user is writing or modifying code that imports `kerfjs`, asks to add a feature to a kerf app, or asks "how do I do X in kerf?". Use it proactively the moment you spot a kerf import in the file you're editing.
4
- kerf-skill-version: 1.3.0
4
+ kerf-skill-version: 1.8.0
5
5
  ---
6
6
 
7
7
  # Building apps with kerf
@@ -47,12 +47,13 @@ import { arraySignal } from 'kerfjs/array-signal';
47
47
  | `morph(liveRoot, template)` | one-shot reconcile against a populated element (SSR hydration, page-refresh diffs). Template = `Element`, `SafeHtml`, or HTML string |
48
48
  | `each(items, render, cacheKey?)` | keyed list iteration; per-row memoization on identity (+ optional cacheKey — a passive comparator for external state). Distinct from `data-key` on the rendered element |
49
49
  | `delegate(root, type, sel, h)` | one listener at the root; `closest(selector)` walk from target |
50
- | `delegateCapture(root, type, sel, h)` | capture-phase escape hatch; `target.matches()` strict match |
50
+ | `delegateCapture(root, type, sel, h, opts?)` | capture-phase escape hatch; `closest()` walk-up by default (same as `delegate`); pass `{ match: 'direct' }` for strict `target.matches()` |
51
51
  | `attr(name, value)` | pre-computed `AttrSpec<N,V>` — `.selector` for `delegate()`, `.attrs` to spread into JSX (rename-safe) |
52
52
  | `attr(name)` | dynamic factory — `attr<N,V=string>(name)` returns `(value: V) => { readonly [name]: V }`; both generics off → N inferred, V defaults to string; specify both to constrain values |
53
53
  | `toElement(jsx)` | parse JSX into a DOM node (SVG-aware). Single-root → `Element`; multi-root (`<><svg/> label</>`, two icons side by side) → `DocumentFragment` that `appendChild`/`replaceChildren`/`append` inlines into the parent. |
54
54
  | `raw(html)` | inject pre-escaped HTML |
55
55
  | `arraySignal(initial?)` | granular keyed-list signal (subpath `kerfjs/array-signal`); `each()` reconciles in O(patches) |
56
+ | `` html`…` `` | tagged template (subpath `kerfjs/html`) — JSX-identical runtime semantics with NO build step, for CDN/importmap projects. Real HTML attribute names (`class`, not `className`); holes only in text positions or as a COMPLETE attribute value (`attr=${v}` / `attr="${v}"`) |
56
57
 
57
58
  ## Hard rules — every AI assistant gets these wrong at least once
58
59
 
@@ -66,7 +67,7 @@ import { arraySignal } from 'kerfjs/array-signal';
66
67
  5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
67
68
  6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX.
68
69
  7. **Components are plain functions.** `<MyComponent props />` works — the JSX runtime calls `MyComponent(props)` and uses the returned JSX — but there's no hook system, no lifecycle, and no per-instance state. State lives in module-scope signals or stores, never in component closures.
69
- 8. **Signal reads must happen INSIDE the render function** to be tracked. `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. Move the read inside.
70
+ 8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX *structure* depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `KERF_DEV_WARN_STALE_BINDING=1`). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders whose only differences were text/attribute values.
70
71
  9. **Store actions take `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
71
72
  10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root.
72
73
  11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise. For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
@@ -81,7 +82,7 @@ When deciding which primitive to reach for, work down the axes:
81
82
  **Events.**
82
83
  - Originates inside the mount tree → `delegate(rootEl, type, sel, handler)`. Originates outside (window-level keyboard, online/offline, beforeunload) → native `window.addEventListener` at module top-level.
83
84
  - Gesture that needs to follow an element after press (drag, draw, resize) → at the start event, `el.setPointerCapture(e.pointerId)`. Subsequent `pointermove` / `pointerup` redirect to the captured element and `delegate(rootEl, 'pointermove', '[data-card]', …)` still picks them up. Don't reach for `window.addEventListener` for in-mount-tree gestures.
84
- - Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or strict element-match → `delegateCapture()`.
85
+ - Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or capture-phase interception → `delegateCapture()` (also `closest()`-matched by default). Need strict element-match? Add `{ match: 'direct' }` on either helper.
85
86
 
86
87
  **Lists.**
87
88
  - Items change across renders (todos, chat messages, table rows) → `each(items, render)`.
@@ -98,17 +99,23 @@ When deciding which primitive to reach for, work down the axes:
98
99
  - User-controlled HTML → sanitize first (DOMPurify) then `raw(sanitized)`.
99
100
  - Author-controlled trusted HTML → `raw(html)` directly.
100
101
 
102
+ **Dangerous URLs.** `javascript:`/`vbscript:`/script-executing `data:` values on `href`/`src`/`xlink:href`/`formaction`/`action`/`data` are dropped — kerf **throws in dev**, **warns + drops in prod**. Sanitize user URLs upstream; wrap an intentional trusted one in `raw(url)` (bypasses the screen in both modes).
103
+
101
104
  ## Canonical patterns
102
105
 
103
106
  ```tsx
104
- // Pattern 1: signal + mount + delegate
107
+ // Pattern 1: signal + mount + delegate.
108
+ // THE core idiom — values bind, structure re-renders: pass the signal ITSELF
109
+ // into a value hole ({count}, class={sig}) so kerf updates that one node
110
+ // directly with no render re-run; read `.value` only when the JSX STRUCTURE
111
+ // depends on the signal (conditionals, list shape).
105
112
  const count = signal(0);
106
113
  const ACTIONS = { inc: attr('data-action', 'inc') } as const satisfies Record<string, AttrSpec<'data-action'>>;
107
114
 
108
115
  mount(document.getElementById('app')!, () => (
109
116
  <div>
110
117
  <button {...ACTIONS.inc.attrs}>+</button>
111
- <span>{count.value}</span>
118
+ <span>{count}</span>
112
119
  </div>
113
120
  ));
114
121
  delegate(rootEl, 'click', ACTIONS.inc.selector, () => { count.value += 1; });
@@ -146,6 +153,15 @@ mount(listEl, () => (
146
153
  </ul>
147
154
  ));
148
155
  // selectedId.value = 3 → only the ~2 affected <li> class attrs update.
156
+
157
+ // Pattern 6: no build step (CDN / importmap) — the html tagged template
158
+ // instead of JSX. Same runtime semantics; real HTML attribute names; a hole
159
+ // must be a text position or a COMPLETE attribute value (partials throw).
160
+ import { html } from 'kerfjs/html';
161
+ mount(rootEl, () => html`
162
+ <div class="${cls}">Count: ${count}</div>
163
+ <ul>${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}</ul>
164
+ `);
149
165
  ```
150
166
 
151
167
  ## Diagnosing common errors
@@ -163,6 +179,7 @@ mount(listEl, () => (
163
179
  | Drag/drop / state change has no visible effect; only elements *outside* `each()` update | Used `each(STATIC_ARRAY, …)` whose row render reads signals. Items never change identity → cache hits forever → row render never re-invoked → signal reads stop tracking | Replace outer with `STATIC_ARRAY.map(...)`; keep inner `each()` for the dynamic sub-list. See Hard Rule 14 |
164
180
  | Row-enter CSS animation no longer replays when only a row's *content* changed (kerf ≥ 0.15.0) | 0.15.0+ morphs a same-identity, same-position row *in place* instead of recreating its node, so a mount-keyed `@keyframes` never re-triggers on a content-only update (≤ 0.14.x recreated the node, so it fired). Intentional flip side: focus, scroll, IME, and in-progress transitions now survive | Key the animation on a state-class toggle, not element creation. To force a remount, churn the row's identity (new object ref / `data-key`) so the reconciler replaces the node |
165
181
  | Want a hot spot to update without re-running the whole render | Fine-grained binding: pass the signal/`computed` ITSELF into the attr/text hole (`class={computed(() => …)}`), not `.value`. Use `computed()` not a bare `() => …` (memoization keeps a shared-signal flip to ~O(changed nodes)). Opt-in per hole. Limit: a bound hole depending on the row's OWN mutated data goes stale on a granular in-place update — use plain interpolation there |
182
+ | `` html`` ``: partial attribute values are not supported | In `kerfjs/html` templates a hole must be the COMPLETE attribute value | Build the full string first (`` class="${`a ${b}`}" ``), or bind `class="${computed(() => `a ${b.value}`)}"` for a reactive one |
166
183
 
167
184
  ## Workflow guidance
168
185
 
@@ -1,4 +1,4 @@
1
- import { signal } from './chunk-4E26PO2C.js';
1
+ import { signal } from './chunk-NU7YHYEV.js';
2
2
 
3
3
  // src/array-signal.ts
4
4
  var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
@@ -1,4 +1,18 @@
1
- import { effect, isSignal } from './chunk-4E26PO2C.js';
1
+ import { effect, isSignal, isDevMode } from './chunk-NU7YHYEV.js';
2
+
3
+ // src/utils/syncFormProp.ts
4
+ function syncFormProp(el, name, value, present) {
5
+ const tag = el.tagName;
6
+ if (name === "checked") {
7
+ if (tag === "INPUT") el.checked = present;
8
+ } else if (name === "value") {
9
+ if (tag === "INPUT" && el !== document.activeElement) {
10
+ el.value = present ? value : "";
11
+ }
12
+ } else if (name === "selected") {
13
+ if (tag === "OPTION") el.selected = present;
14
+ }
15
+ }
2
16
 
3
17
  // src/utils/urlScreen.ts
4
18
  var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction", "action", "data"]);
@@ -30,6 +44,11 @@ function isDangerousUrlValue(name, value) {
30
44
  function dangerousUrlWarning(name, value) {
31
45
  return `dropped dangerous URL value for ${name}=${JSON.stringify(value.slice(0, 80))}. kerf blocks javascript:, vbscript:, and script-executing data: URLs (text/html, image/svg+xml, xml) in href/src/data/formaction/action/xlink:href by default. Wrap in raw() if this is intentional (e.g. bookmarklets), or sanitize upstream.`;
32
46
  }
47
+ function reportDangerousUrl(context2, name, value) {
48
+ const message = `${context2}: ${dangerousUrlWarning(name, value)}`;
49
+ if (isDevMode()) throw new Error(message);
50
+ console.warn(message);
51
+ }
33
52
 
34
53
  // src/bindings.ts
35
54
  var BIND_ATTR = "data-kfb";
@@ -39,7 +58,7 @@ var ROW_TEXT_PREFIX = "kfbr:";
39
58
  var context = null;
40
59
  var rowSink = null;
41
60
  var rowCounter = 0;
42
- var NO_DISPOSERS = [];
61
+ var NO_DISPOSERS = Object.freeze([]);
43
62
  function newBindingContext() {
44
63
  return { counter: 0, list: [] };
45
64
  }
@@ -92,7 +111,7 @@ function wireBindings(rootEl, ctx, prevDisposers) {
92
111
  for (const d of prevDisposers) d();
93
112
  if (ctx.list.length === 0) return NO_DISPOSERS;
94
113
  const disposers = [];
95
- wireInto(rootEl, BIND_ATTR, TEXT_MARKER_PREFIX, ctx.list, disposers);
114
+ wireInto(rootEl, ctx.list, disposers);
96
115
  return disposers;
97
116
  }
98
117
  function wireRowBindings(rowNode, bindings) {
@@ -138,10 +157,29 @@ function disposeRowBindings(disposers) {
138
157
  if (disposers === void 0) return;
139
158
  for (const d of disposers) d();
140
159
  }
141
- function wireInto(scope, attrName, textPrefix, bindings, disposers) {
142
- const attrEls = indexAttrEls(scope, attrName);
160
+ function carryOrRewireRowBindings(node, oldBindings, oldDisposers, newBindings) {
161
+ const oldLen = oldBindings === void 0 ? 0 : oldBindings.length;
162
+ const newLen = newBindings === void 0 ? 0 : newBindings.length;
163
+ if (oldLen === newLen) {
164
+ let same = true;
165
+ for (let i = 0; i < newLen; i++) {
166
+ if (oldBindings[i].signal !== newBindings[i].signal) {
167
+ same = false;
168
+ break;
169
+ }
170
+ }
171
+ if (same) return { bindings: oldBindings, bindingDisposers: oldDisposers };
172
+ }
173
+ disposeRowBindings(oldDisposers);
174
+ return {
175
+ bindings: newBindings,
176
+ bindingDisposers: newLen > 0 ? wireRowBindings(node, newBindings) : void 0
177
+ };
178
+ }
179
+ function wireInto(scope, bindings, disposers) {
180
+ const attrEls = indexAttrEls(scope, BIND_ATTR);
143
181
  const textMarkers = /* @__PURE__ */ new Map();
144
- collectComments(scope, textPrefix, textMarkers);
182
+ collectComments(scope, TEXT_MARKER_PREFIX, textMarkers);
145
183
  for (const b of bindings) {
146
184
  if (b.kind === "attr") {
147
185
  const el = attrEls.get(b.id);
@@ -164,33 +202,47 @@ function indexAttrEls(scope, attrName) {
164
202
  function attachAttrEffect(el, attr, signal) {
165
203
  return effect(() => setBoundAttr(el, attr, signal.value));
166
204
  }
205
+ var insertedTextNodes = /* @__PURE__ */ new WeakMap();
206
+ function boundTextNodeOf(marker) {
207
+ const t = insertedTextNodes.get(marker);
208
+ return t !== void 0 && marker.nextSibling === t ? t : null;
209
+ }
167
210
  function attachTextEffect(marker, signal) {
168
- const text = marker.ownerDocument.createTextNode("");
169
- marker.parentNode.insertBefore(text, marker.nextSibling);
211
+ let text = insertedTextNodes.get(marker);
212
+ if (text === void 0 || marker.nextSibling !== text) {
213
+ text = marker.ownerDocument.createTextNode("");
214
+ marker.parentNode.insertBefore(text, marker.nextSibling);
215
+ insertedTextNodes.set(marker, text);
216
+ }
217
+ const node = text;
170
218
  return effect(() => {
171
- text.data = coerceText(signal.value);
219
+ node.data = coerceText(signal.value);
172
220
  });
173
221
  }
174
222
  function setBoundAttr(el, name, value) {
175
223
  if (value == null || value === false) {
176
224
  el.removeAttribute(name);
225
+ syncFormProp(el, name, "", false);
177
226
  return;
178
227
  }
179
228
  if (value === true) {
180
229
  el.setAttribute(name, "");
230
+ syncFormProp(el, name, "", true);
181
231
  return;
182
232
  }
183
233
  if (isSafeHtmlValue(value)) {
184
234
  el.setAttribute(name, value.__html);
235
+ syncFormProp(el, name, value.__html, true);
185
236
  return;
186
237
  }
187
238
  const str = String(value);
188
239
  if (isDangerousUrlValue(name, str)) {
189
- console.warn(`kerf binding: ${dangerousUrlWarning(name, str)}`);
240
+ reportDangerousUrl("kerf binding", name, str);
190
241
  el.removeAttribute(name);
191
242
  return;
192
243
  }
193
244
  el.setAttribute(name, str);
245
+ syncFormProp(el, name, str, true);
194
246
  }
195
247
  var SAFE_HTML_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
196
248
  function isSafeHtmlValue(v) {
@@ -212,17 +264,18 @@ function collectComments(node, prefix, out) {
212
264
  }
213
265
 
214
266
  // src/segment.ts
267
+ var LIST_MARKER_PREFIX = "kf-list:";
215
268
  function flatten(segment, withMarkers) {
216
269
  if (segment.kind === "static") return segment.html;
217
270
  if (segment.kind === "list") {
218
271
  const items = segment.items.map((i) => i.html).join("");
219
- return withMarkers ? `<!--kf-list:${segment.id}-->${items}` : items;
272
+ return withMarkers ? `<!--${LIST_MARKER_PREFIX}${segment.id}-->${items}` : items;
220
273
  }
221
274
  return segment.parts.map((p) => flatten(p, withMarkers)).join("");
222
275
  }
223
276
  function flattenWithoutListItems(segment) {
224
277
  if (segment.kind === "static") return segment.html;
225
- if (segment.kind === "list") return `<!--kf-list:${segment.id}-->`;
278
+ if (segment.kind === "list") return `<!--${LIST_MARKER_PREFIX}${segment.id}-->`;
226
279
  return segment.parts.map(flattenWithoutListItems).join("");
227
280
  }
228
281
  function collectLists(segment, out = /* @__PURE__ */ new Map()) {
@@ -488,7 +541,9 @@ See docs/5-event-delegation.md for the tier-1/tier-2/tier-3 model.`
488
541
  }
489
542
  }
490
543
  function renderAttr(key, value) {
491
- const name = ATTR_ALIASES[key] ?? key;
544
+ return renderAttrNamed(key, ATTR_ALIASES[key] ?? key, value);
545
+ }
546
+ function renderAttrNamed(key, name, value) {
492
547
  if (value == null || value === false) return "";
493
548
  assertEmittableAttrName(key, name, typeof value === "function");
494
549
  if (value === true) return ` ${name}`;
@@ -499,7 +554,7 @@ function renderAttr(key, value) {
499
554
  strValue = String(value);
500
555
  } else if (typeof value === "string") {
501
556
  if (isDangerousUrlValue(name, value)) {
502
- console.warn(`JSX: ${dangerousUrlWarning(name, value)}`);
557
+ reportDangerousUrl("JSX", name, value);
503
558
  return "";
504
559
  }
505
560
  strValue = escapeAttr(value);
@@ -537,7 +592,13 @@ function jsx(tag, props) {
537
592
  function Fragment({ children }) {
538
593
  return new SafeHtml(children != null ? toSegment(children) : { kind: "static", html: "" });
539
594
  }
595
+ function _toSegment(child) {
596
+ return toSegment(child);
597
+ }
598
+ function _renderAttrVerbatim(name, value) {
599
+ return renderAttrNamed(name, name, value);
600
+ }
540
601
 
541
- export { Fragment, SafeHtml, _setBindingContext, captureRowBindings, collectLists, disposeRowBindings, flatten, flattenWithoutListItems, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, newBindingContext, raw, wireBindings, wireRowBindings };
542
- //# sourceMappingURL=chunk-QNYOMGI4.js.map
543
- //# sourceMappingURL=chunk-QNYOMGI4.js.map
602
+ export { Fragment, LIST_MARKER_PREFIX, SafeHtml, _renderAttrVerbatim, _setBindingContext, _toSegment, assertEmittableAttrName, bindAttr, bindMarkerAttr, boundTextNodeOf, captureRowBindings, carryOrRewireRowBindings, collectLists, disposeRowBindings, flatten, flattenWithoutListItems, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, mergeChildSegments, newBindingContext, raw, syncFormProp, wireBindings, wireRowBindings };
603
+ //# sourceMappingURL=chunk-GYRZQCSY.js.map
604
+ //# sourceMappingURL=chunk-GYRZQCSY.js.map