kerfjs 1.0.2 → 2.0.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/templateParse.ts","../src/html.ts"],"names":[],"mappings":"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","file":"html.js","sourcesContent":["/**\n * Static-parts parser for the `kerfjs/html` tagged template (`html\\`\\``).\n *\n * Parses a template's static string chunks ONCE into a `ParsedTemplate`:\n * the (possibly trimmed) chunks plus an ordered hole-descriptor list that\n * classifies each `${…}` hole as a TEXT hole (a child position) or an ATTR\n * hole (a complete attribute value, `attr=${v}` / `attr=\"${v}\"`). The\n * renderer in `src/html.ts` caches the parse per template-strings-array\n * identity, so repeated renders of the same call site skip this entirely.\n *\n * The state machine is deliberately small and strict: it tracks only\n * text / inside-open-tag / inside-comment modes and quote state. Holes in\n * any position it can't prove safe — tag names, attribute names, partial\n * attribute values, comments — THROW with an actionable message rather\n * than guessing. Static chunks are author-written markup and pass through\n * verbatim (the same trust model as JSX tag/attribute names).\n */\n\nexport interface AttrHole {\n kind: 'attr';\n /** The attribute name scanned from the preceding static chunk, verbatim. */\n name: string;\n /** The surrounding quote character, or null for an unquoted `attr=${v}`. */\n quote: '\"' | \"'\" | null;\n}\n\nexport interface TextHole {\n kind: 'text';\n}\n\nexport type TemplateHole = AttrHole | TextHole;\n\nexport interface ParsedTemplate {\n /**\n * Static chunks with attr-hole scaffolding (`name=`, quotes) stripped —\n * the renderer re-emits the whole ` name=\"value\"` via the JSX attribute\n * renderer. Always `holes.length + 1` entries.\n */\n chunks: string[];\n holes: TemplateHole[];\n /**\n * Per-chunk offset of the `>` that closes the element tag open at the\n * chunk's start, or -1. Only chunks that follow an attr hole can start\n * inside a tag; the renderer uses this to inject the bound-attribute\n * marker (`data-kfb` / `data-kfbrow`) for signal attribute holes.\n */\n tagClose: number[];\n}\n\nconst TEXT_HOLE: TextHole = { kind: 'text' };\n\n/** Matches a chunk tail of `name=` optionally followed by an opening quote. */\nconst ATTR_TAIL = /(\\s*)([^\\s\"'<>/=]+)=([\"'])?$/;\n\nfunction holeError(detail: string): Error {\n return new Error(`html\\`\\`: ${detail}`);\n}\n\nfunction partialValueError(tail: string): Error {\n return holeError(\n `partial attribute values are not supported (near ${JSON.stringify(tail.slice(-30))}) — `\n + 'a hole must be the COMPLETE attribute value: attr=${v} or attr=\"${v}\". '\n + 'For class=\"a ${b}\"-style composition, build the full string first '\n + '(a plain template literal, or computed(() => `a ${b.value}`) for a bound attribute).',\n );\n}\n\nexport function parseTemplate(strings: readonly string[]): ParsedTemplate {\n const chunks: string[] = new Array(strings.length);\n const holes: TemplateHole[] = new Array(strings.length - 1);\n const tagClose: number[] = new Array<number>(strings.length).fill(-1);\n let mode: 'text' | 'tag' | 'comment' = 'text';\n let quote: '\"' | \"'\" | null = null;\n\n for (let i = 0; i < strings.length; i++) {\n let s = strings[i];\n // A quoted attr hole (`attr=\"${v}\"`) must be immediately closed by its\n // quote — the renderer emits the full quoted attribute itself, so the\n // author's closing quote is consumed here.\n if (i > 0) {\n const prev = holes[i - 1];\n if (prev.kind === 'attr' && prev.quote !== null) {\n if (s[0] !== prev.quote) throw partialValueError(strings[i - 1]);\n s = s.slice(1);\n }\n }\n\n const startedInTag = mode === 'tag';\n for (let j = 0; j < s.length; j++) {\n const ch = s[j];\n if (mode === 'text') {\n if (ch === '<') {\n if (s.startsWith('<!--', j)) {\n mode = 'comment';\n j += 3;\n } else if (j + 1 < s.length && /[a-zA-Z!/?]/.test(s[j + 1])) {\n mode = 'tag';\n }\n }\n } else if (mode === 'tag') {\n if (quote !== null) {\n if (ch === quote) quote = null;\n } else if (ch === '\"' || ch === \"'\") {\n quote = ch;\n } else if (ch === '>') {\n if (startedInTag && tagClose[i] === -1) tagClose[i] = j;\n mode = 'text';\n }\n } else if (ch === '-' && s.startsWith('-->', j)) {\n mode = 'text';\n j += 2;\n }\n }\n\n // Classify the hole that follows this chunk (no hole after the last one).\n if (i < strings.length - 1) {\n if (mode === 'comment') {\n throw holeError(\n 'holes inside HTML comments are not supported — move the ${…} hole outside the <!-- --> comment.',\n );\n }\n if (mode === 'text') {\n if (/<\\/?$/.test(s)) {\n throw holeError(\n 'tag-name holes (`<${…}>`) are not supported — write tag names statically. '\n + 'For a literal \"<\" before a hole, escape it as &lt;.',\n );\n }\n holes[i] = TEXT_HOLE;\n } else {\n const m = ATTR_TAIL.exec(s);\n if (quote !== null) {\n // Inside a quoted attribute value: only valid if the quote opened\n // as the chunk's very last character (i.e. the hole IS the value).\n if (m === null || m[3] === undefined) throw partialValueError(s);\n holes[i] = { kind: 'attr', name: m[2], quote: m[3] as '\"' | \"'\" };\n s = s.slice(0, s.length - m[0].length);\n quote = null;\n } else if (m !== null && m[3] === undefined) {\n // Unquoted `attr=${v}`: the next static chunk must resume with an\n // attribute delimiter, or the template may simply end there.\n const next = strings[i + 1];\n const validNext = next.length > 0 ? /^[\\s>/]/.test(next) : i + 1 === strings.length - 1;\n if (!validNext) throw partialValueError(s);\n holes[i] = { kind: 'attr', name: m[2], quote: null };\n s = s.slice(0, s.length - m[0].length);\n } else if (/<\\/?$/.test(s)) {\n // The tag opened as the chunk's last characters (`</${…}`).\n throw holeError(\n 'tag-name holes (`<${…}>`) are not supported — write tag names statically. '\n + 'For a literal \"<\" before a hole, escape it as &lt;.',\n );\n } else {\n throw holeError(\n 'a hole inside a tag must be a complete attribute value — attr=${…} or attr=\"${…}\". '\n + 'Tag-name and attribute-name holes are not supported; write those statically.',\n );\n }\n }\n }\n chunks[i] = s;\n }\n return { chunks, holes, tagClose };\n}\n","/**\n * `html` — tagged-template authoring at the `kerfjs/html` subpath, so\n * \"no build step\" is literally true: a CDN / importmap consumer can author\n * kerf UIs without a JSX transform.\n *\n * import { html } from 'kerfjs/html';\n * html`<div class=\"${cls}\">Count: ${count}</div>`\n * html`<ul>${each(items.value, (i) => html`<li id=\"${i.id}\">${i.label}</li>`)}</ul>`\n *\n * A thin front-end over the exact machinery JSX uses — the runtime paths are\n * IDENTICAL. Text holes go through the JSX child pipeline (`_toSegment`):\n * SafeHtml/list-segment passthrough (so `each()` composes and the keyed\n * reconciler owns its rows), string escaping, number stringify, boolean /\n * nullish → nothing, signal → fine-grained text binding, DOM-node and\n * unsupported-type errors. Attribute holes go through the JSX attribute\n * branch: signal → `_assertEmittableAttrName` + `bindAttr` (grouped into one\n * marker attribute per element); anything else → the JSX attribute renderer\n * (booleans, SafeHtml, URL screening, `on*` / malformed-name rejection).\n * `mount()` / `morph()` / the reconcilers need zero changes.\n *\n * Unlike JSX, NO camelCase attribute aliasing is applied — template authors\n * write real HTML attribute names (`class`, not `className`).\n *\n * Hole contract (enforced by the parser in `utils/templateParse.ts`): holes\n * are allowed in text/child positions and as a COMPLETE attribute value\n * (`attr=${v}` or `attr=\"${v}\"`). Tag-name holes, attribute-name holes,\n * partial attribute values (`class=\"a ${b}\"`), and holes inside comments\n * throw. Static chunks are author-written markup and pass through verbatim\n * (same trust model as JSX tags/attrs).\n *\n * Perf: the parse runs once per template call site — the tagged-template\n * strings array has stable identity, so a `WeakMap` keyed on it caches the\n * `ParsedTemplate`. Rendering is a chunk walk with string concatenation,\n * the same cost shape as the JSX runtime.\n */\n\nimport { bindAttr, bindMarkerAttr, isSignal } from './bindings.js';\nimport {\n _assertEmittableAttrName,\n _renderAttrVerbatim,\n _toSegment,\n SafeHtml,\n} from './jsx-runtime.js';\nimport type { ReadonlySignal, Signal } from './reactive.js';\nimport { mergeChildSegments, type Segment } from './segment.js';\nimport { type ParsedTemplate, parseTemplate } from './utils/templateParse.js';\n\n/**\n * Values accepted in `html\\`\\`` holes — the same set JSX accepts for\n * children (text holes) and attribute values (attr holes), including a\n * signal/`computed` itself for a fine-grained binding.\n */\nexport type HtmlValue =\n | SafeHtml\n | string\n | number\n | boolean\n | null\n | undefined\n | ReadonlySignal<unknown>\n | readonly HtmlValue[];\n\nconst PARSE_CACHE = new WeakMap<TemplateStringsArray, ParsedTemplate>();\n\n// Module-level mutable counter, sanctioned as observability-only state (see\n// CLAUDE.md design rule 5): it feeds the `_parseCount()` test hook that pins\n// the parse-once-per-callsite cache behavior and never influences rendering.\nlet parseCount = 0;\n\n/** Test hook: number of template parses performed (cache misses) so far. */\nexport function _parseCount(): number {\n return parseCount;\n}\n\nfunction getParsed(strings: TemplateStringsArray): ParsedTemplate {\n let parsed = PARSE_CACHE.get(strings);\n if (parsed === undefined) {\n parsed = parseTemplate(strings);\n parseCount++;\n PARSE_CACHE.set(strings, parsed);\n }\n return parsed;\n}\n\nexport function html(strings: TemplateStringsArray, ...values: HtmlValue[]): SafeHtml {\n const { chunks, holes, tagClose } = getParsed(strings);\n const parts: Segment[] = [];\n let buf = '';\n // Signal-attr binding ids for the currently-open element; flushed as one\n // marker attribute (`data-kfb` / `data-kfbrow`) at the tag's closing `>`,\n // mirroring the per-element grouping in `jsx()`.\n let pendingBindIds: string[] | null = null;\n\n for (let i = 0; i < chunks.length; i++) {\n let chunk = chunks[i];\n if (pendingBindIds !== null && tagClose[i] !== -1) {\n const off = tagClose[i];\n chunk = `${chunk.slice(0, off)} ${bindMarkerAttr()}=\"${pendingBindIds.join(',')}\"${chunk.slice(off)}`;\n pendingBindIds = null;\n }\n buf += chunk;\n if (i === holes.length) break;\n\n const hole = holes[i];\n const value = values[i];\n if (hole.kind === 'text') {\n const seg = _toSegment(value);\n if (seg.kind === 'static') {\n buf += seg.html;\n } else {\n parts.push({ kind: 'static', html: buf });\n buf = '';\n parts.push(seg);\n }\n } else if (isSignal(value)) {\n // Same contract as the `jsx()` signal-attribute branch: reject `on*` /\n // malformed names unconditionally (a signal's value changes over time),\n // then register the binding — or snapshot outside a mount render.\n _assertEmittableAttrName(hole.name, hole.name, false);\n const id = bindAttr(hole.name, value as Signal<unknown>);\n if (id !== null) {\n (pendingBindIds ??= []).push(id);\n } else {\n buf += _renderAttrVerbatim(hole.name, (value as Signal<unknown>).value);\n }\n } else {\n buf += _renderAttrVerbatim(hole.name, value);\n }\n }\n if (buf !== '' || parts.length === 0) parts.push({ kind: 'static', html: buf });\n return new SafeHtml(mergeChildSegments(parts));\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -5,6 +5,30 @@ import { Signal } from '@preact/signals-core';
5
5
  export { ReadonlySignal, Signal, batch, computed } from '@preact/signals-core';
6
6
  export { S as Store, d as defineStore, r as resetAllStores } from './testing-DNEY7wi3.js';
7
7
 
8
+ /**
9
+ * Re-exports of `@preact/signals-core`. Lets the rest of the codebase depend
10
+ * on `'./reactive.js'` without naming the underlying lib, so swapping it out
11
+ * later (or fronting it with a hand-rolled implementation) is a one-file
12
+ * change.
13
+ *
14
+ * Two dev-gated wrappers sit in front of the bare re-exports:
15
+ *
16
+ * - `signal()` returns a `DevSignal` when `KERF_DEV_WARN_UNTRACKED_SIGNALS=1`
17
+ * (KF-176) — warns on writes to signals with no subscribers.
18
+ *
19
+ * - `effect()` wraps the user body in `enterEffect()` / `exitEffect()` calls
20
+ * when `KERF_DEV_WARN_DELEGATE_IN_EFFECT=1` so `delegate()` can detect when
21
+ * it's running inside an effect body and fire the appropriate warning.
22
+ *
23
+ * Both gates short-circuit when `isDevMode()` is false (i.e. under
24
+ * `NODE_ENV === 'production'`, or a `globalThis.KERF_DEV = false` override) —
25
+ * production always sees the bare `@preact/signals-core` exports with zero
26
+ * overhead.
27
+ */
28
+
29
+ declare function signal<T>(value?: T): Signal<T>;
30
+ declare function effect(fn: () => void | (() => void)): () => void;
31
+
8
32
  /**
9
33
  * `attr(name, value)` — create a pre-computed attribute descriptor (static form).
10
34
  * `attr(name)` — create a per-render factory for dynamic attribute values (dynamic form).
@@ -27,7 +51,8 @@ export { S as Store, d as defineStore, r as resetAllStores } from './testing-DNE
27
51
  * **Dynamic form** — best for per-row data like `data-id`, where the value
28
52
  * changes per item but the attribute name is constant.
29
53
  * The name is validated and pre-escaped at definition time; calling the
30
- * returned factory is cheap (just escape the value and freeze the object).
54
+ * returned factory is cheap (it just freezes a one-key object — the value is
55
+ * escaped later by the JSX attribute renderer when the result is spread).
31
56
  *
32
57
  * const ITEM = { id: attr('data-id') } as const;
33
58
  *
@@ -43,7 +68,8 @@ export { S as Store, d as defineStore, r as resetAllStores } from './testing-DNE
43
68
  * Escaping:
44
69
  * - Attribute name: escaped as a CSS identifier via `cssEscapeIdent`, which is
45
70
  * an SSR-safe (no `CSS.escape`) adaptation of the Mathias Bynens polyfill
46
- * (https://github.com/nicktindall/cyclon.p2p-common, MIT licensed). Handles
71
+ * (https://github.com/mathiasbynens/CSS.escape, MIT licensed — see the
72
+ * Acknowledgements section of LICENSE). Handles
47
73
  * control chars, leading digits, non-ASCII, and CSS metacharacters.
48
74
  * - Attribute value: embedded in double quotes as a CSS string. Backslashes and
49
75
  * double-quote characters are backslash-escaped; control characters are
@@ -102,14 +128,32 @@ declare function attr<N extends string, V extends string = string>(name: N): (va
102
128
  * a descendant `<input>`.
103
129
  *
104
130
  * - Tier 2 (explicit capture) — use `delegateCapture()`.
105
- * The escape hatch for cases the auto-promotion list doesn't cover, or
106
- * when you want capture-phase semantics and direct `matches()`-style
107
- * selector matching (no walk-up).
131
+ * The escape hatch for cases the auto-promotion list doesn't cover
132
+ * (custom non-bubbling events) or when you want capture-phase
133
+ * interception. Selector matching is `closest()`-style by default —
134
+ * the same walk-up as `delegate()`, and it passes the matched ancestor
135
+ * (not the raw target) to the handler — so a click on any descendant of
136
+ * the selected element climbs to it. Pass `{ match: 'direct' }` to opt
137
+ * into strict `matches()`-style matching (fire only when the event lands
138
+ * on the exact element the selector identifies).
108
139
  *
109
140
  * - Tier 3 (per-element instances / library-owned subtrees) — mark the
110
141
  * host element with `data-morph-skip` and manage the library's
111
142
  * lifecycle directly. No delegation helper applies.
112
143
  */
144
+ /**
145
+ * How the selector is matched against the event's target:
146
+ *
147
+ * - `'closest'` (the default for both helpers) — walk UP from `event.target`
148
+ * via `closest(selector)`, firing for the nearest matching ancestor inside
149
+ * `rootEl`. This is the delegation behavior you almost always want: a click
150
+ * on an icon inside a button fires the button's handler.
151
+ * - `'direct'` — strict `matches()` match: fire only when `event.target`
152
+ * itself matches the selector, with no walk-up.
153
+ */
154
+ interface DelegateOptions {
155
+ match?: 'closest' | 'direct';
156
+ }
113
157
  /**
114
158
  * Delegation that "just works" for both bubbling and the common non-bubbling
115
159
  * events. Installs ONE listener on `rootEl`; for known non-bubblers (see
@@ -118,6 +162,9 @@ declare function attr<N extends string, V extends string = string>(name: N): (va
118
162
  * matching walks up from `event.target` via `closest(selector)` and fires
119
163
  * `handler(event, matched)` if the match is inside `rootEl`.
120
164
  *
165
+ * Pass `{ match: 'direct' }` to fire only when `event.target` itself matches
166
+ * the selector (no walk-up); the default is `'closest'`.
167
+ *
121
168
  * The generic `T` narrows the second handler argument to the expected element
122
169
  * type — `delegate<HTMLButtonElement>(root, 'click', 'button', (e, btn) => btn.value)`
123
170
  * — so consumers can avoid casts. Defaults to `Element` for untyped calls.
@@ -128,41 +175,29 @@ declare function attr<N extends string, V extends string = string>(name: N): (va
128
175
  * delegate(rootEl, 'click', '[data-action="add"]', handlerFn);
129
176
  * delegate(rootEl, 'focus', 'input', handlerFn); // auto-capture
130
177
  */
131
- declare function delegate<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void): () => void;
178
+ declare function delegate<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void, options?: DelegateOptions): () => void;
132
179
  /**
133
- * Capture-phase delegation — for non-bubbling events (`focus`, `blur`,
134
- * `scroll`, `load`, `error`). Reaches descendants of `rootEl` that match
135
- * `selector` regardless of how many times the diff has rebuilt them.
180
+ * Capture-phase delegation — the escape hatch for custom non-bubbling events
181
+ * (ones `delegate()`'s auto-promotion list doesn't know about) and for
182
+ * capture-phase interception (run before any descendant's bubble-phase
183
+ * handler). Reaches descendants of `rootEl` that match `selector` regardless
184
+ * of how many times the diff has rebuilt them.
185
+ *
186
+ * Selector matching is `closest()`-style by default — the same walk-up as
187
+ * `delegate()`, and it passes the matched ancestor (not the raw target) to
188
+ * the handler — so a click on any descendant of the selected element climbs
189
+ * to it. Pass `{ match: 'direct' }` to opt into strict `matches()`-style
190
+ * matching (fire only when the event lands on the exact element the selector
191
+ * identifies, with no walk-up).
136
192
  *
137
193
  * The generic `T` narrows the second handler argument to the expected element
138
194
  * type, mirroring `delegate<T>()`. Defaults to `Element` for untyped calls.
139
195
  *
140
196
  * Usage (pseudo-code — see examples for live ones):
141
197
  * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);
198
+ * delegateCapture(rootEl, 'click', '.exact', handlerFn, { match: 'direct' });
142
199
  */
143
- declare function delegateCapture<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void): () => void;
144
-
145
- /**
146
- * Re-exports of `@preact/signals-core`. Lets the rest of the codebase depend
147
- * on `'./reactive.js'` without naming the underlying lib, so swapping it out
148
- * later (or fronting it with a hand-rolled implementation) is a one-file
149
- * change.
150
- *
151
- * Two dev-gated wrappers sit in front of the bare re-exports:
152
- *
153
- * - `signal()` returns a `DevSignal` when `KERF_DEV_WARN_UNTRACKED_SIGNALS=1`
154
- * (KF-176) — warns on writes to signals with no subscribers.
155
- *
156
- * - `effect()` wraps the user body in `enterEffect()` / `exitEffect()` calls
157
- * when `KERF_DEV_WARN_DELEGATE_IN_EFFECT=1` so `delegate()` can detect when
158
- * it's running inside an effect body and fire the appropriate warning.
159
- *
160
- * Both gates short-circuit on `NODE_ENV === 'production'` — production
161
- * always sees the bare `@preact/signals-core` exports with zero overhead.
162
- */
163
-
164
- declare function signal<T>(value?: T): Signal<T>;
165
- declare function effect(fn: () => void | (() => void)): () => void;
200
+ declare function delegateCapture<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void, options?: DelegateOptions): () => void;
166
201
 
167
202
  /**
168
203
  * `each(items, render, cacheKey?)` — keyed list iteration with per-item memoization.
@@ -291,6 +326,8 @@ declare function morph(liveRoot: Element, template: Element | SafeHtml | string,
291
326
  * position are not destroyed and recreated on each tick.
292
327
  */
293
328
 
329
+ /** What `mount()`'s render function may return; non-SafeHtml values coerce (nullish/boolean → render nothing). */
330
+ type MountResult = SafeHtml | string | number | boolean | null | undefined;
294
331
  /**
295
332
  * Bind `render()` to the children of `rootEl`. Re-runs whenever any signal
296
333
  * read inside `render()` changes. Returns a disposer that tears down the
@@ -314,7 +351,6 @@ declare function morph(liveRoot: Element, template: Element | SafeHtml | string,
314
351
  * else they did to the DOM — survives verbatim. The next render after
315
352
  * blur catches up.
316
353
  */
317
- type MountResult = SafeHtml | string | number | boolean | null | undefined;
318
354
  declare function mount(rootEl: HTMLElement, render: () => MountResult): () => void;
319
355
 
320
356
  /**
@@ -343,4 +379,4 @@ declare function mount(rootEl: HTMLElement, render: () => MountResult): () => vo
343
379
 
344
380
  declare function toElement(jsx: SafeHtml | string): Element | DocumentFragment;
345
381
 
346
- export { type AttrSpec, type MountResult, SafeHtml, attr, delegate, delegateCapture, each, effect, morph, mount, signal, toElement };
382
+ export { type AttrSpec, type DelegateOptions, type MountResult, SafeHtml, attr, delegate, delegateCapture, each, effect, morph, mount, signal, toElement };