kerfjs 0.2.0 → 0.3.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.
- package/CHANGELOG.md +14 -0
- package/LICENSE +32 -0
- package/README.md +4 -4
- package/dist/{chunk-URMYMSGU.js → chunk-ZLV35OHG.js} +97 -16
- package/dist/chunk-ZLV35OHG.js.map +1 -0
- package/dist/index.d.ts +57 -11
- package/dist/index.js +300 -30
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +70 -6
- package/dist/jsx-runtime.js +1 -1
- package/package.json +5 -3
- package/dist/chunk-URMYMSGU.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to **kerf** are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-05-08
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
- Rebuilt render pipeline with structured segments and a native keyed-list diff, replacing the morphdom dependency
|
|
11
|
+
- Added `each()` for keyed list iteration with per-item HTML memoisation by object identity
|
|
12
|
+
|
|
13
|
+
## [0.2.1] - 2026-05-07
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
- Add `Fragment` export to the `kerfjs` barrel for explicit JSX fragment usage
|
|
17
|
+
|
|
7
18
|
## [0.2.0] - 2026-05-07
|
|
8
19
|
|
|
9
20
|
|
|
@@ -25,18 +36,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
25
36
|
|
|
26
37
|
### Fixed
|
|
27
38
|
|
|
39
|
+
- **`Fragment` was missing from the `kerfjs` barrel (KF-24).** `Fragment` was implemented in `src/jsx-runtime.ts`, exported from `kerfjs/jsx-runtime`, and present in the shared chunk — but the barrel `src/index.ts` didn't re-export it. Importing `Fragment` from `'kerfjs'` resolved to `undefined`, so a manual `<Fragment>...</Fragment>` rendered as `<undefined>...</undefined>`. The `<>...</>` shorthand was unaffected because the JSX transform pulls `Fragment` from `kerfjs/jsx-runtime` directly. Added `Fragment` to the barrel re-export, and pinned the entire public-API contract with a new `tests/dist/barrel-completeness.test.ts` so any future omission fails CI loudly. Docs updated to list `Fragment` in the public API surface (`CLAUDE.md`, `llms.txt`, `docs/ai/usage-guide.md`, `docs/ai/code-summary.md`, `docs/6-jsx-runtime.md`, `docs/8-api-reference.md`).
|
|
28
40
|
- **Focused contenteditable was being morphed, clobbering in-progress edits (KF-19).** The docs claimed contenteditable elements got focus + selection preservation alongside `<input>` and `<textarea>`, but the implementation only handled the latter two — a focused contenteditable's typed content was overwritten by morphdom on the next re-render. `mount()` now short-circuits the morph entirely when the active element is a contenteditable (same mechanism as `data-morph-skip`), so the user's edit, caret position, and any multi-range selection survive verbatim. Attribute updates are deferred until the next render after blur — that's the explicit trade-off, and matches what you want for in-progress rich-text editing. `docs/4-render.md` §4.4 and `docs/8-api-reference.md` §8.7 updated to describe the per-element-kind behaviour. The check uses the `contenteditable` attribute directly (the spec's source of truth) rather than the derived `isContentEditable` property, so test environments that don't populate the latter still get correct behaviour.
|
|
29
41
|
- **`clearStoreRegistry` was a no-op in the published bundle (KF-15).** `dist/testing.js` shipped an empty function body. Root cause: `tsup` bundled each entry independently with `splitting: false`, so the testing entry tree-shook the module-level `REGISTRY` array out as unreferenced — leaving `REGISTRY.length = 0` as dead code. Same root cause as KF-14. Fixed by enabling `splitting: true` in `tsup.config.ts`: shared modules now live in chunk files that all entries import, so `defineStore`'s registry and `clearStoreRegistry`'s reference are the same array. Side benefit: the duplicate `SafeHtml` class definition is gone too — there's now exactly one copy across the whole dist. Build output now includes `dist/chunk-*.js` files (covered by the existing `"files": ["dist"]` in `package.json`). New regression test in `tests/dist/store-registry-shared.test.ts` exercises the cross-entry registry from the built bundles.
|
|
30
42
|
- **`SafeHtml` cross-bundle identity (KF-14).** When a consumer's bundler ended up loading two copies of kerf — for example, the barrel (`kerfjs`) and the JSX-runtime entry (`kerfjs/jsx-runtime`) resolving as separate modules — `instanceof SafeHtml` failed inside the JSX runtime because the two `SafeHtml` classes were structurally identical but referentially distinct. The renderer would then throw `JSX: unsupported child of type object (SafeHtml)` on perfectly valid JSX. `SafeHtml` instances now carry a `Symbol.for('kerfjs.SafeHtml')` brand and the runtime checks for the brand instead of using `instanceof`. New unit tests simulate the duplicate-class scenario, and a new `npm run test:dist` job exercises the actual built bundles in CI.
|
|
31
43
|
|
|
32
44
|
### Added
|
|
33
45
|
|
|
46
|
+
- **`each(items, render, key?)` list primitive** exported from `kerfjs`. Keyed list iteration with per-item memoisation: skips re-running `render` for items whose object identity (and optional `key`) are unchanged since the previous call. Targets the partial-update / select-row / swap-rows perf path, where today's `mount()` re-runs the render for the full list on any signal change. On the js-framework-benchmark suite this drops kerfjs's partial-update from 87 → 64 ms (-27%), select-row from 69 → 42 ms (-38%), swap-rows from 86 → 58 ms (-33%), and remove-row from 49 → 35 ms (-29%); creates and bundle size are unaffected (+0.2 KB gz for the WeakMap memoiser). See `docs/8-api-reference.md` §8.3 and `bench/` for the benchmark harness.
|
|
34
47
|
- **`isSafeHtml(value)` type guard** exported from `kerfjs`. Use this rather than `instanceof SafeHtml` when inspecting JSX values from your own code — it works across module copies.
|
|
35
48
|
- **End-to-end test coverage of the published bundle (KF-16).** New `npm run test:dist:full` re-runs the entire unit + integration suite against `dist/` instead of `src/` via a tiny vitest plugin that rewrites `../../src/<name>.js` imports to the equivalent dist entry point. Wired into the CI `build` job. Combined with the existing `test:dist` (focused dist regression suite), CI now proves the exact bytes we publish pass every test we have, not just the source they were built from.
|
|
36
49
|
- **Four behavioural-guarantee tests (KF-17)** pinning documented contracts that previously had no test: signals are not deep-reactive (§2.6), `batch()` inside an action coalesces notifications (§3.5), `mount()` disposer leaves the rendered DOM in place (§4), and direct event listeners inside `data-morph-skip` subtrees survive parent re-renders (Tier 3, §5).
|
|
37
50
|
|
|
38
51
|
### Changed
|
|
39
52
|
|
|
53
|
+
- **Render pipeline rebuilt around structured segments + a native diff.** `SafeHtml` no longer wraps a flat string; it wraps a `Segment` tree that distinguishes `static` HTML, `list` segments (from `each(...)`), and `mixed` parents containing lists. `mount()` dispatches on the segment kind: static surrounds go through a new general-purpose tree-diff (`src/diff.ts`, derived from morphdom — MIT — with attribution in `LICENSE`), and lists are reconciled directly against live children by a keyed reconciler. The reconciler bulk-parses every fresh row's HTML in one `innerHTML` call, then uses an LIS pass over old positions so the number of `insertBefore` calls is the minimum possible. `morphdom` is no longer a runtime dependency — kerf now depends only on `@preact/signals-core`. Net perf vs the prior `each` + morphdom Stage-1: partial-update 64 → 51 ms (-19%), select-row 42 → 39 ms (-9%), swap-rows 58 → 33 ms (-43%), remove-row 35 → 21 ms (-39%), append-1k 67 → 54 ms (-19%), clear 35 → 23 ms (-33%); creates roughly unchanged. Bundle gz: 6.9 → 6.6 KB. Public API and JSX usage are unchanged — the change is entirely internal.
|
|
40
54
|
- **Package renamed from `kerf` to `kerfjs`** on the npm registry. The `kerf` name was rejected by npm's typo-squatting heuristic ("too similar to `keyv`"). The brand is still *kerf* — only the npm identifier changed. Update imports to `from 'kerfjs'`, `tsconfig.json` to `"jsxImportSource": "kerfjs"`, and the install command to `npm install kerfjs`. The GitHub repo and Pages URL (`brianwestphal.github.io/kerf/`) are unchanged.
|
|
41
55
|
|
|
42
56
|
### Added
|
package/LICENSE
CHANGED
|
@@ -19,3 +19,35 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Acknowledgements
|
|
27
|
+
|
|
28
|
+
`src/diff.ts` re-implements the DOM-reconciliation algorithm of
|
|
29
|
+
[morphdom](https://github.com/patrick-steele-idem/morphdom) by Patrick
|
|
30
|
+
Steele-Idem, which is also distributed under the MIT License:
|
|
31
|
+
|
|
32
|
+
The MIT License (MIT)
|
|
33
|
+
|
|
34
|
+
Copyright (c) Patrick Steele-Idem <pnidem@gmail.com>
|
|
35
|
+
|
|
36
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
37
|
+
a copy of this software and associated documentation files (the
|
|
38
|
+
"Software"), to deal in the Software without restriction, including
|
|
39
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
40
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
41
|
+
permit persons to whom the Software is furnished to do so, subject
|
|
42
|
+
to the following conditions:
|
|
43
|
+
|
|
44
|
+
The above copyright notice and this permission notice shall be
|
|
45
|
+
included in all copies or substantial portions of the Software.
|
|
46
|
+
|
|
47
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
48
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
49
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
50
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
51
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
52
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
53
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ mount(document.getElementById('app')!, () => (
|
|
|
19
19
|
));
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
That's it. There's no virtual DOM, no compiler, no template language. Your JSX renders to HTML strings
|
|
22
|
+
That's it. There's no virtual DOM, no compiler, no template language. Your JSX renders to HTML strings (with structured "list" segments where you use `each(...)`), 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.
|
|
23
23
|
|
|
24
24
|
## Why
|
|
25
25
|
|
|
@@ -27,10 +27,10 @@ Most reactive UI frameworks come with a lot of machinery: virtual DOMs, schedule
|
|
|
27
27
|
|
|
28
28
|
- **Signals** ([`@preact/signals-core`](https://github.com/preactjs/signals)) for fine-grained reactivity.
|
|
29
29
|
- **Stores** built on signals — composable, testable units of state.
|
|
30
|
-
- **Render** — a `mount(el, () => jsx)` helper that diffs the new HTML against the live DOM
|
|
30
|
+
- **Render** — a `mount(el, () => jsx)` helper that diffs the new HTML against the live DOM with kerf's native, segment-aware reconciler. Preserves focus, selection, in-flight pointer interactions, and event listeners on identity-preserved nodes. Lists rendered with `each(...)` go through a keyed reconciler that does O(changes) work, not O(rows).
|
|
31
31
|
- **Event delegation** — small `delegate` / `delegateCapture` helpers that survive every re-render because they live on the morph root, not on individual nodes.
|
|
32
32
|
|
|
33
|
-
The whole runtime is roughly
|
|
33
|
+
The whole runtime is roughly 6.6 KB minified + gzipped, including `signals-core`.
|
|
34
34
|
|
|
35
35
|
## Install
|
|
36
36
|
|
|
@@ -101,7 +101,7 @@ The numbered docs in [`docs/`](./docs/) cover the design and rationale:
|
|
|
101
101
|
1. [Overview](./docs/1-overview.md) — what kerf is, what it isn't, when to use it.
|
|
102
102
|
2. [Reactivity](./docs/2-reactivity.md) — `signal`, `computed`, `effect`, `batch`.
|
|
103
103
|
3. [Stores](./docs/3-stores.md) — `defineStore`, `resetAllStores`.
|
|
104
|
-
4. [Render](./docs/4-render.md) — `mount
|
|
104
|
+
4. [Render](./docs/4-render.md) — `mount`, segments, the native diff, and the list reconciler.
|
|
105
105
|
5. [Event delegation](./docs/5-event-delegation.md) — Tier 1 / Tier 2 / Tier 3 patterns.
|
|
106
106
|
6. [JSX runtime](./docs/6-jsx-runtime.md) — `SafeHtml`, `raw`, server-rendering.
|
|
107
107
|
7. [SVG handling](./docs/7-svg.md) — namespace propagation, `toElement`.
|
|
@@ -1,3 +1,72 @@
|
|
|
1
|
+
// src/segment.ts
|
|
2
|
+
function flatten(segment, withMarkers) {
|
|
3
|
+
if (segment.kind === "static") return segment.html;
|
|
4
|
+
if (segment.kind === "list") {
|
|
5
|
+
const items = segment.items.map((i) => i.html).join("");
|
|
6
|
+
return withMarkers ? `<!--kf-list:${segment.id}-->${items}` : items;
|
|
7
|
+
}
|
|
8
|
+
return segment.parts.map((p) => flatten(p, withMarkers)).join("");
|
|
9
|
+
}
|
|
10
|
+
function flattenWithoutListItems(segment) {
|
|
11
|
+
if (segment.kind === "static") return segment.html;
|
|
12
|
+
if (segment.kind === "list") return `<!--kf-list:${segment.id}-->`;
|
|
13
|
+
return segment.parts.map(flattenWithoutListItems).join("");
|
|
14
|
+
}
|
|
15
|
+
function collectLists(segment, out = /* @__PURE__ */ new Map()) {
|
|
16
|
+
if (segment.kind === "list") out.set(segment.id, segment);
|
|
17
|
+
else if (segment.kind === "mixed") {
|
|
18
|
+
for (const part of segment.parts) collectLists(part, out);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function mergeChildSegments(parts) {
|
|
23
|
+
if (parts.length === 0) return { kind: "static", html: "" };
|
|
24
|
+
if (parts.every((p) => p.kind === "static")) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "static",
|
|
27
|
+
html: parts.map((p) => p.html).join("")
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const merged = [];
|
|
31
|
+
let coalesced = "";
|
|
32
|
+
for (const p of parts) {
|
|
33
|
+
if (p.kind === "static") {
|
|
34
|
+
coalesced += p.html;
|
|
35
|
+
} else {
|
|
36
|
+
if (coalesced !== "") {
|
|
37
|
+
merged.push({ kind: "static", html: coalesced });
|
|
38
|
+
coalesced = "";
|
|
39
|
+
}
|
|
40
|
+
merged.push(p);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (coalesced !== "") merged.push({ kind: "static", html: coalesced });
|
|
44
|
+
return { kind: "mixed", parts: merged };
|
|
45
|
+
}
|
|
46
|
+
function wrapWithTags(child, openTag, closeTag) {
|
|
47
|
+
if (child.kind === "static") {
|
|
48
|
+
return { kind: "static", html: openTag + child.html + closeTag };
|
|
49
|
+
}
|
|
50
|
+
if (child.kind === "mixed") {
|
|
51
|
+
return {
|
|
52
|
+
kind: "mixed",
|
|
53
|
+
parts: [
|
|
54
|
+
{ kind: "static", html: openTag },
|
|
55
|
+
...child.parts,
|
|
56
|
+
{ kind: "static", html: closeTag }
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
kind: "mixed",
|
|
62
|
+
parts: [
|
|
63
|
+
{ kind: "static", html: openTag },
|
|
64
|
+
child,
|
|
65
|
+
{ kind: "static", html: closeTag }
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
1
70
|
// src/utils/escapeHtml.ts
|
|
2
71
|
function escapeHtml(str) {
|
|
3
72
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -107,10 +176,17 @@ var ATTR_ALIASES = {
|
|
|
107
176
|
var SAFE_HTML_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
|
|
108
177
|
var SafeHtml = class {
|
|
109
178
|
__html;
|
|
179
|
+
__segment;
|
|
110
180
|
// Branded so `isSafeHtml()` recognises instances from any copy of this module.
|
|
111
181
|
[SAFE_HTML_BRAND] = true;
|
|
112
|
-
constructor(
|
|
113
|
-
|
|
182
|
+
constructor(input) {
|
|
183
|
+
if (typeof input === "string") {
|
|
184
|
+
this.__segment = { kind: "static", html: input };
|
|
185
|
+
this.__html = input;
|
|
186
|
+
} else {
|
|
187
|
+
this.__segment = input;
|
|
188
|
+
this.__html = flatten(input, false);
|
|
189
|
+
}
|
|
114
190
|
}
|
|
115
191
|
toString() {
|
|
116
192
|
return this.__html;
|
|
@@ -122,6 +198,9 @@ function isSafeHtml(value) {
|
|
|
122
198
|
function raw(html) {
|
|
123
199
|
return new SafeHtml(html);
|
|
124
200
|
}
|
|
201
|
+
function listSafeHtml(id, items) {
|
|
202
|
+
return new SafeHtml({ kind: "list", id, items });
|
|
203
|
+
}
|
|
125
204
|
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
126
205
|
"area",
|
|
127
206
|
"base",
|
|
@@ -137,20 +216,22 @@ var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
|
137
216
|
"track",
|
|
138
217
|
"wbr"
|
|
139
218
|
]);
|
|
140
|
-
function
|
|
141
|
-
if (
|
|
142
|
-
if (isSafeHtml(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
219
|
+
function toSegment(child) {
|
|
220
|
+
if (child == null || typeof child === "boolean") return { kind: "static", html: "" };
|
|
221
|
+
if (isSafeHtml(child)) {
|
|
222
|
+
return child.__segment ?? { kind: "static", html: child.__html };
|
|
223
|
+
}
|
|
224
|
+
if (typeof child === "string") return { kind: "static", html: escapeHtml(child) };
|
|
225
|
+
if (typeof child === "number") return { kind: "static", html: String(child) };
|
|
226
|
+
if (Array.isArray(child)) return mergeChildSegments(child.map(toSegment));
|
|
227
|
+
const maybeNode = child;
|
|
147
228
|
if (typeof maybeNode === "object" && maybeNode !== null && ("nodeType" in maybeNode || "outerHTML" in maybeNode)) {
|
|
148
229
|
throw new Error(
|
|
149
230
|
"JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). Build the tree in one JSX expression and use querySelector after toElement() to get element refs."
|
|
150
231
|
);
|
|
151
232
|
}
|
|
152
233
|
throw new Error(
|
|
153
|
-
`JSX: unsupported child of type ${describeValue(
|
|
234
|
+
`JSX: unsupported child of type ${describeValue(child)}. Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), passing a function (call it first), or passing a Promise (await it before render).`
|
|
154
235
|
);
|
|
155
236
|
}
|
|
156
237
|
function describeValue(v) {
|
|
@@ -184,13 +265,13 @@ function jsx(tag, props) {
|
|
|
184
265
|
const { children, ...attrs } = props;
|
|
185
266
|
const attrStr = Object.entries(attrs).map(([k, v]) => renderAttr(k, v)).join("");
|
|
186
267
|
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
|
|
187
|
-
const
|
|
188
|
-
return new SafeHtml(`<${tag}${attrStr}
|
|
268
|
+
const childSegment = children != null ? toSegment(children) : { kind: "static", html: "" };
|
|
269
|
+
return new SafeHtml(wrapWithTags(childSegment, `<${tag}${attrStr}>`, `</${tag}>`));
|
|
189
270
|
}
|
|
190
271
|
function Fragment({ children }) {
|
|
191
|
-
return new SafeHtml(children != null ?
|
|
272
|
+
return new SafeHtml(children != null ? toSegment(children) : { kind: "static", html: "" });
|
|
192
273
|
}
|
|
193
274
|
|
|
194
|
-
export { Fragment, SafeHtml, isSafeHtml, jsx, raw };
|
|
195
|
-
//# sourceMappingURL=chunk-
|
|
196
|
-
//# sourceMappingURL=chunk-
|
|
275
|
+
export { Fragment, SafeHtml, collectLists, flatten, flattenWithoutListItems, isSafeHtml, jsx, listSafeHtml, raw };
|
|
276
|
+
//# sourceMappingURL=chunk-ZLV35OHG.js.map
|
|
277
|
+
//# sourceMappingURL=chunk-ZLV35OHG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/segment.ts","../src/utils/escapeHtml.ts","../src/utils/jsx-attr-aliases.ts","../src/jsx-runtime.ts"],"names":[],"mappings":"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","file":"chunk-ZLV35OHG.js","sourcesContent":["/**\n * `Segment` — kerf's structured render output.\n *\n * The JSX runtime emits a `SafeHtml` wrapping a `Segment`. Most renders\n * produce a single static segment (just an HTML string), which behaves\n * exactly like a string for backward compatibility. When the tree\n * contains a list (`each()`) or a parent whose children include a list,\n * the runtime emits a structured segment that `mount()` can dispatch\n * on — running its native keyed reconciler for the list parts and\n * leaving the static surrounds to the general-purpose diff.\n *\n * Why have a structured form at all: the perf bottleneck for huge\n * keyed lists isn't the per-row JSX work (which `each()` already\n * memoises). It's that flattening every render's whole tree to one\n * big HTML string forces a full `innerHTML` parse and a tree walk\n * over rows we know are unchanged. The segment shape lets mount()\n * skip both for the list parts.\n */\n\nexport type Segment = StaticSegment | ListSegment | MixedSegment;\n\nexport interface StaticSegment {\n kind: 'static';\n html: string;\n}\n\nexport interface ListItem {\n /**\n * The row's object identity. Used by the reconciler to match new items\n * against live DOM nodes across renders. Unchanged ref → reuse the\n * existing live node; replaced ref → build a fresh node.\n */\n ref: object;\n /**\n * Optional cache-invalidation key that captures external state affecting\n * this row's render (e.g. selection class). Different cacheKey on the\n * same `ref` triggers a cache miss for that row. `undefined` when the\n * user didn't pass a `key` callback to `each()`.\n */\n cacheKey: unknown;\n html: string;\n}\n\nexport interface ListSegment {\n kind: 'list';\n id: string;\n items: ListItem[];\n}\n\nexport interface MixedSegment {\n kind: 'mixed';\n parts: Segment[];\n}\n\n/**\n * Flatten a segment to a complete HTML string. Used for first render\n * (bulk innerHTML), for SSR-style consumption via `toString()`, and\n * for diagnostics.\n *\n * If `withMarkers` is set, list segments are wrapped in\n * `<!--kf-list:{id}-->` comments so the post-parse walk can find each\n * list's live parent. Plain (non-marker) flatten is what JSX consumers\n * see when they call `.toString()` on the SafeHtml.\n */\nexport function flatten(segment: Segment, withMarkers: boolean): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list') {\n const items = segment.items.map((i) => i.html).join('');\n return withMarkers ? `<!--kf-list:${segment.id}-->${items}` : items;\n }\n return segment.parts.map((p) => flatten(p, withMarkers)).join('');\n}\n\n/**\n * Variant of `flatten` for the static-only diff path on subsequent\n * renders. Lists are reduced to a single marker comment with no items\n * inside — the actual list children stay in the live DOM and are\n * reconciled separately. Keeping list items out of this string is\n * what makes the morph cheap on huge lists where most rows are\n * unchanged.\n */\nexport function flattenWithoutListItems(segment: Segment): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list') return `<!--kf-list:${segment.id}-->`;\n return segment.parts.map(flattenWithoutListItems).join('');\n}\n\n/** Collect every `ListSegment` in the tree, keyed by its id. */\nexport function collectLists(\n segment: Segment,\n out: Map<string, ListSegment> = new Map(),\n): Map<string, ListSegment> {\n if (segment.kind === 'list') out.set(segment.id, segment);\n else if (segment.kind === 'mixed') {\n for (const part of segment.parts) collectLists(part, out);\n }\n return out;\n}\n\n/**\n * Combine a list of child segments into the smallest equivalent\n * representation: collapses adjacent statics into one static, returns\n * a single static if everything is static, otherwise a mixed segment\n * with statics coalesced.\n */\nexport function mergeChildSegments(parts: Segment[]): Segment {\n if (parts.length === 0) return { kind: 'static', html: '' };\n if (parts.every((p) => p.kind === 'static')) {\n return {\n kind: 'static',\n html: parts.map((p) => (p as StaticSegment).html).join(''),\n };\n }\n const merged: Segment[] = [];\n let coalesced = '';\n for (const p of parts) {\n if (p.kind === 'static') {\n coalesced += p.html;\n } else {\n if (coalesced !== '') {\n merged.push({ kind: 'static', html: coalesced });\n coalesced = '';\n }\n merged.push(p);\n }\n }\n if (coalesced !== '') merged.push({ kind: 'static', html: coalesced });\n return { kind: 'mixed', parts: merged };\n}\n\n/**\n * Wrap a child segment with surrounding open/close tags from the\n * parent JSX element. Used by the JSX runtime when constructing\n * `_jsx(tag, ...)` output.\n */\nexport function wrapWithTags(child: Segment, openTag: string, closeTag: string): Segment {\n if (child.kind === 'static') {\n return { kind: 'static', html: openTag + child.html + closeTag };\n }\n if (child.kind === 'mixed') {\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n ...child.parts,\n { kind: 'static', html: closeTag },\n ],\n };\n }\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n child,\n { kind: 'static', html: closeTag },\n ],\n };\n}\n","/**\n * HTML / attribute escaping for the JSX runtime. Identical to the helpers\n * used in any reasonable HTML emitter — included here so kerf has no extra\n * runtime dependencies beyond `@preact/signals-core` and `morphdom`.\n */\n\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n}\n","/**\n * JSX → HTML / SVG attribute name aliases.\n *\n * The JSX runtime translates camelCase attributes (React convention) to\n * the kebab-case / colon-form names the browser actually wants. Anything\n * not in this map is passed through verbatim — `data-*`, `aria-*`, and\n * any custom attribute work without ceremony.\n *\n * Lives in its own module so `src/jsx-runtime.ts` can stay under the\n * 200-LOC project guideline; the bulk of `jsx-runtime.ts` was this table.\n */\n\nexport const ATTR_ALIASES: Record<string, string> = {\n // HTML attributes\n className: 'class',\n htmlFor: 'for',\n httpEquiv: 'http-equiv',\n acceptCharset: 'accept-charset',\n accessKey: 'accesskey',\n autoCapitalize: 'autocapitalize',\n autoComplete: 'autocomplete',\n autoFocus: 'autofocus',\n autoPlay: 'autoplay',\n colSpan: 'colspan',\n contentEditable: 'contenteditable',\n crossOrigin: 'crossorigin',\n dateTime: 'datetime',\n defaultChecked: 'checked',\n defaultValue: 'value',\n encType: 'enctype',\n formAction: 'formaction',\n formEncType: 'formenctype',\n formMethod: 'formmethod',\n formNoValidate: 'formnovalidate',\n formTarget: 'formtarget',\n hrefLang: 'hreflang',\n inputMode: 'inputmode',\n maxLength: 'maxlength',\n minLength: 'minlength',\n noModule: 'nomodule',\n noValidate: 'novalidate',\n readOnly: 'readonly',\n referrerPolicy: 'referrerpolicy',\n rowSpan: 'rowspan',\n spellCheck: 'spellcheck',\n srcDoc: 'srcdoc',\n srcLang: 'srclang',\n srcSet: 'srcset',\n tabIndex: 'tabindex',\n useMap: 'usemap',\n\n // SVG presentation attributes (camelCase → kebab-case)\n strokeWidth: 'stroke-width',\n strokeLinecap: 'stroke-linecap',\n strokeLinejoin: 'stroke-linejoin',\n strokeDasharray: 'stroke-dasharray',\n strokeDashoffset: 'stroke-dashoffset',\n strokeMiterlimit: 'stroke-miterlimit',\n strokeOpacity: 'stroke-opacity',\n fillOpacity: 'fill-opacity',\n fillRule: 'fill-rule',\n clipPath: 'clip-path',\n clipRule: 'clip-rule',\n colorInterpolation: 'color-interpolation',\n colorInterpolationFilters: 'color-interpolation-filters',\n floodColor: 'flood-color',\n floodOpacity: 'flood-opacity',\n lightingColor: 'lighting-color',\n stopColor: 'stop-color',\n stopOpacity: 'stop-opacity',\n shapeRendering: 'shape-rendering',\n imageRendering: 'image-rendering',\n textRendering: 'text-rendering',\n pointerEvents: 'pointer-events',\n vectorEffect: 'vector-effect',\n paintOrder: 'paint-order',\n\n // SVG text/font attributes\n fontFamily: 'font-family',\n fontSize: 'font-size',\n fontStyle: 'font-style',\n fontVariant: 'font-variant',\n fontWeight: 'font-weight',\n fontStretch: 'font-stretch',\n textAnchor: 'text-anchor',\n textDecoration: 'text-decoration',\n dominantBaseline: 'dominant-baseline',\n alignmentBaseline: 'alignment-baseline',\n baselineShift: 'baseline-shift',\n letterSpacing: 'letter-spacing',\n wordSpacing: 'word-spacing',\n writingMode: 'writing-mode',\n\n // SVG marker attributes\n markerStart: 'marker-start',\n markerMid: 'marker-mid',\n markerEnd: 'marker-end',\n\n // SVG xlink (legacy but still used)\n xlinkHref: 'xlink:href',\n xlinkShow: 'xlink:show',\n xlinkActuate: 'xlink:actuate',\n xlinkType: 'xlink:type',\n xlinkRole: 'xlink:role',\n xlinkTitle: 'xlink:title',\n xlinkArcrole: 'xlink:arcrole',\n xmlBase: 'xml:base',\n xmlLang: 'xml:lang',\n xmlSpace: 'xml:space',\n xmlnsXlink: 'xmlns:xlink',\n};\n","/**\n * kerf JSX runtime.\n *\n * JSX renders to `SafeHtml`, which wraps both:\n * - `__html`: the flattened HTML string (what `toString()` returns; what\n * legacy/SSR consumers care about)\n * - `__segment`: a structured representation that distinguishes \"static\n * html\", \"keyed list\", and \"mixed\" content.\n *\n * Most renders are pure-static and the segment is just `{kind:'static',html}`.\n * When the tree contains a list (via `each()`) or a parent whose children\n * include a non-static segment, the runtime threads that structure up so\n * `mount()` can dispatch on it — running its native keyed reconciler for\n * the list parts and leaving the static surrounds to the general-purpose\n * diff.\n *\n * Configure in your `tsconfig.json`:\n *\n * \"jsx\": \"react-jsx\",\n * \"jsxImportSource\": \"kerfjs\"\n *\n * Then write JSX as you normally would — kerf provides the `jsx` /\n * `jsxs` / `jsxDEV` / `Fragment` exports the JSX transform looks for.\n */\n\nimport {\n flatten,\n type ListSegment,\n mergeChildSegments,\n type Segment,\n wrapWithTags,\n} from './segment.js';\nimport { escapeAttr, escapeHtml } from './utils/escapeHtml.js';\nimport { ATTR_ALIASES } from './utils/jsx-attr-aliases.js';\n\n// Cross-realm/cross-bundle brand. Using `Symbol.for` (the global registry)\n// means two `SafeHtml` classes from different module copies still recognise\n// each other. Same approach React uses for `$$typeof: Symbol.for('react.element')`.\n// Without this, `instanceof SafeHtml` fails when the consumer's bundler ends\n// up loading two copies of kerf (separate barrel + jsx-runtime entries,\n// monorepo dedup misses, ESM/CJS interop, etc.).\nconst SAFE_HTML_BRAND = Symbol.for('kerfjs.SafeHtml');\n\nexport class SafeHtml {\n readonly __html: string;\n readonly __segment: Segment;\n // Branded so `isSafeHtml()` recognises instances from any copy of this module.\n readonly [SAFE_HTML_BRAND] = true as const;\n constructor(input: string | Segment) {\n if (typeof input === 'string') {\n this.__segment = { kind: 'static', html: input };\n this.__html = input;\n } else {\n this.__segment = input;\n this.__html = flatten(input, false);\n }\n }\n toString(): string {\n return this.__html;\n }\n}\n\n/**\n * Type guard for `SafeHtml`. Prefer this over `instanceof SafeHtml` — it works\n * across module copies (e.g. when the consumer's bundler loads kerf's barrel\n * and JSX-runtime entries as independent modules).\n */\nexport function isSafeHtml(value: unknown): value is SafeHtml {\n return typeof value === 'object'\n && value !== null\n && (value as Record<symbol, unknown>)[SAFE_HTML_BRAND] === true;\n}\n\n/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */\nexport function raw(html: string): SafeHtml {\n return new SafeHtml(html);\n}\n\n/**\n * Internal: build a `SafeHtml` representing a list segment. Used by\n * `each()` so the JSX runtime is the sole owner of `SafeHtml` construction.\n */\nexport function listSafeHtml(id: string, items: ListSegment['items']): SafeHtml {\n return new SafeHtml({ kind: 'list', id, items });\n}\n\ntype Child = SafeHtml | string | number | boolean | null | undefined;\ntype Children = Child | Children[];\n\ninterface Props {\n children?: Children;\n [key: string]: unknown;\n}\n\nconst VOID_TAGS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'source', 'track', 'wbr',\n]);\n\n/**\n * Convert a single JSX child into a Segment. Handles SafeHtml passthrough,\n * primitive coercion + escaping, arrays (recursive), and the nullish/false\n * skip cases.\n */\nfunction toSegment(child: Children): Segment {\n if (child == null || typeof child === 'boolean') return { kind: 'static', html: '' };\n if (isSafeHtml(child)) {\n // Cross-bundle SafeHtml shims (KF-14 case) may have only `__html`.\n return child.__segment ?? { kind: 'static', html: child.__html };\n }\n if (typeof child === 'string') return { kind: 'static', html: escapeHtml(child) };\n if (typeof child === 'number') return { kind: 'static', html: String(child) };\n if (Array.isArray(child)) return mergeChildSegments(child.map(toSegment));\n // Catch the common mistake of passing a DOM element (e.g. the result of\n // toElement(...)) as a JSX child. The runtime renders to HTML strings, so\n // DOM nodes can't be composed — they'd silently serialize to \"\" and their\n // event listeners would be lost. Throw loudly so this can't sneak in.\n const maybeNode = child as unknown;\n if (typeof maybeNode === 'object' && maybeNode !== null\n && ('nodeType' in maybeNode || 'outerHTML' in maybeNode)) {\n throw new Error(\n 'JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). '\n + 'Build the tree in one JSX expression and use querySelector after toElement() to get element refs.',\n );\n }\n throw new Error(\n `JSX: unsupported child of type ${describeValue(child)}. `\n + 'Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. '\n + 'Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), '\n + 'passing a function (call it first), or passing a Promise (await it before render).',\n );\n}\n\nfunction describeValue(v: unknown): string {\n if (Array.isArray(v)) return 'array';\n if (typeof v === 'object' && v !== null) {\n const ctor = (v as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== 'Object' ? `object (${ctor})` : 'object';\n }\n return typeof v;\n}\n\nfunction renderAttr(key: string, value: unknown): string {\n const name = ATTR_ALIASES[key] ?? key;\n if (value == null || value === false) return '';\n if (value === true) return ` ${name}`;\n let strValue: string;\n if (isSafeHtml(value)) {\n strValue = value.__html;\n } else if (typeof value === 'number') {\n strValue = String(value);\n } else if (typeof value === 'string') {\n strValue = escapeAttr(value);\n } else {\n throw new Error(\n `JSX: unsupported value for attribute \"${key}\" — got ${describeValue(value)}. `\n + 'Attribute values must be string, number, boolean, null, undefined, or SafeHtml. '\n + 'Did you mean to read .value off a Signal, or stringify the object first?',\n );\n }\n return ` ${name}=\"${strValue}\"`;\n}\n\nexport function jsx(tag: string | ((props: Props) => SafeHtml), props: Props): SafeHtml {\n if (typeof tag === 'function') return tag(props);\n\n const { children, ...attrs } = props;\n const attrStr = Object.entries(attrs)\n .map(([k, v]) => renderAttr(k, v))\n .join('');\n\n if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);\n\n const childSegment: Segment = children != null\n ? toSegment(children)\n : { kind: 'static', html: '' };\n return new SafeHtml(wrapWithTags(childSegment, `<${tag}${attrStr}>`, `</${tag}>`));\n}\n\nexport { jsx as jsxs };\n// vitest's dev-mode JSX transform emits `jsxDEV(tag, props, ...)`; the\n// alias lets tests import this module without the production build pipeline\n// caring.\nexport { jsx as jsxDEV };\n\nexport function Fragment({ children }: { children?: Children }): SafeHtml {\n return new SafeHtml(children != null ? toSegment(children) : { kind: 'static', html: '' });\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace JSX {\n export type Element = SafeHtml;\n export interface ElementChildrenAttribute {\n children: unknown;\n }\n export interface IntrinsicElements {\n [elemName: string]: Record<string, unknown>;\n }\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SafeHtml } from './jsx-runtime.js';
|
|
2
|
-
export { isSafeHtml, raw } from './jsx-runtime.js';
|
|
2
|
+
export { Fragment, isSafeHtml, raw } from './jsx-runtime.js';
|
|
3
3
|
export { ReadonlySignal, Signal, batch, computed, effect, signal } from '@preact/signals-core';
|
|
4
4
|
export { S as Store, d as defineStore, r as resetAllStores } from './testing-CdMgVVoI.js';
|
|
5
5
|
|
|
@@ -47,19 +47,65 @@ declare function delegate(rootEl: HTMLElement, type: string, selector: string, h
|
|
|
47
47
|
*/
|
|
48
48
|
declare function delegateCapture(rootEl: HTMLElement, type: string, selector: string, handler: Handler): () => void;
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* `each(items, render, key?)` — keyed list iteration with per-item memoisation.
|
|
52
|
+
*
|
|
53
|
+
* Drops in as the body of a list-rendering JSX expression inside a `mount()`
|
|
54
|
+
* render function. Returns a `SafeHtml` carrying a structured list segment,
|
|
55
|
+
* so `mount()` can run a native keyed reconciler instead of the general-
|
|
56
|
+
* purpose morph for these children.
|
|
57
|
+
*
|
|
58
|
+
* Two layers of optimisation:
|
|
59
|
+
*
|
|
60
|
+
* 1. Per-item memoisation. `render(item)` is skipped for items whose object
|
|
61
|
+
* identity (and optional `key`) are unchanged since the previous call.
|
|
62
|
+
* Their HTML strings come from a `WeakMap` keyed by item reference. The
|
|
63
|
+
* immutable-update style ("replace the row object" instead of "mutate it")
|
|
64
|
+
* makes the cache work automatically.
|
|
65
|
+
*
|
|
66
|
+
* 2. Structural handoff. `mount()` recognises the list segment and bypasses
|
|
67
|
+
* the parse-the-whole-table round trip: only fresh items get parsed (one
|
|
68
|
+
* at a time, into the smallest detached element), and only changed rows
|
|
69
|
+
* get patched in the live DOM. Unchanged rows are physically the same
|
|
70
|
+
* nodes they were before — never visited.
|
|
71
|
+
*
|
|
72
|
+
* `key` covers the case where external state, not the item itself, drives
|
|
73
|
+
* what the row should render (e.g. a "currently selected" id flips a CSS
|
|
74
|
+
* class on one row). Same item identity but a different `key` value means
|
|
75
|
+
* "re-render this item." If you don't pass `key`, only identity changes
|
|
76
|
+
* invalidate.
|
|
77
|
+
*
|
|
78
|
+
* Items must be objects (cache is a `WeakMap`); wrap primitives if you need
|
|
79
|
+
* to iterate them. Each item's render output must produce exactly one
|
|
80
|
+
* top-level element — the list reconciler binds one live DOM node per item.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
declare function each<T extends object>(items: readonly T[], render: (item: T, index: number) => SafeHtml | string, key?: (item: T, index: number) => unknown): SafeHtml;
|
|
84
|
+
|
|
50
85
|
/**
|
|
51
86
|
* `mount(rootEl, render)` — kerf's render primitive.
|
|
52
87
|
*
|
|
53
|
-
* Wraps `effect()`
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
88
|
+
* Wraps `effect()` so that whenever any signal read inside `render()`
|
|
89
|
+
* changes, we re-run `render()` and apply the minimum DOM mutations against
|
|
90
|
+
* the live tree. Element identity (and thus focus, selection, in-flight
|
|
91
|
+
* pointer interactions, and event listeners on preserved nodes) is preserved
|
|
92
|
+
* wherever the keyed/positional diff matches.
|
|
93
|
+
*
|
|
94
|
+
* Two phases per render:
|
|
95
|
+
*
|
|
96
|
+
* - Static surrounds (everything outside `each()` lists): morphdom diffs
|
|
97
|
+
* a freshly-built template against the live tree. Same conventions as
|
|
98
|
+
* before — id/data-key matching, `data-morph-skip`, focus preservation.
|
|
99
|
+
*
|
|
100
|
+
* - List interiors (children of every `each()` parent): native keyed
|
|
101
|
+
* reconciler operates directly on the live parent's children. No
|
|
102
|
+
* re-parse, no morph walk for cache-hit rows. Cost is O(changes), not
|
|
103
|
+
* O(rows).
|
|
58
104
|
*
|
|
59
|
-
* Compared to a `replaceChildren(...rows.map(toElement))` rebuild pattern,
|
|
60
|
-
* user-visible win is that an `<input>` the user is typing into
|
|
61
|
-
* unrelated re-render — its DOM node, focus state, and cursor
|
|
62
|
-
* not destroyed and recreated on each tick.
|
|
105
|
+
* Compared to a `replaceChildren(...rows.map(toElement))` rebuild pattern,
|
|
106
|
+
* the user-visible win is that an `<input>` the user is typing into
|
|
107
|
+
* survives an unrelated re-render — its DOM node, focus state, and cursor
|
|
108
|
+
* position are not destroyed and recreated on each tick.
|
|
63
109
|
*/
|
|
64
110
|
|
|
65
111
|
/**
|
|
@@ -103,4 +149,4 @@ declare function mount(rootEl: HTMLElement, render: () => SafeHtml | string): ()
|
|
|
103
149
|
|
|
104
150
|
declare function toElement(jsx: SafeHtml | string): Element;
|
|
105
151
|
|
|
106
|
-
export { SafeHtml, delegate, delegateCapture, mount, toElement };
|
|
152
|
+
export { SafeHtml, delegate, delegateCapture, each, mount, toElement };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { isSafeHtml } from './chunk-
|
|
2
|
-
export { SafeHtml, isSafeHtml, raw } from './chunk-
|
|
1
|
+
import { isSafeHtml, listSafeHtml, flatten, collectLists, flattenWithoutListItems } from './chunk-ZLV35OHG.js';
|
|
2
|
+
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-ZLV35OHG.js';
|
|
3
3
|
import { effect } from './chunk-IZJIKRCE.js';
|
|
4
4
|
export { batch, computed, defineStore, effect, resetAllStores, signal } from './chunk-IZJIKRCE.js';
|
|
5
|
-
import morphdom from 'morphdom';
|
|
6
5
|
|
|
7
6
|
// src/delegate.ts
|
|
8
7
|
function assertValidSelector(selector, fn) {
|
|
@@ -43,35 +42,144 @@ function delegateCapture(rootEl, type, selector, handler) {
|
|
|
43
42
|
rootEl.removeEventListener(type, listener, true);
|
|
44
43
|
};
|
|
45
44
|
}
|
|
45
|
+
|
|
46
|
+
// src/each.ts
|
|
47
|
+
var ROW_CACHE = /* @__PURE__ */ new WeakMap();
|
|
48
|
+
var listCounter = null;
|
|
49
|
+
function _setListCounter(c) {
|
|
50
|
+
listCounter = c;
|
|
51
|
+
}
|
|
52
|
+
function each(items, render, key) {
|
|
53
|
+
const id = listCounter !== null ? String(listCounter.value++) : "orphan";
|
|
54
|
+
const segItems = new Array(items.length);
|
|
55
|
+
for (let i = 0; i < items.length; i++) {
|
|
56
|
+
const item = items[i];
|
|
57
|
+
const k = key ? key(item, i) : void 0;
|
|
58
|
+
const cached = ROW_CACHE.get(item);
|
|
59
|
+
let html;
|
|
60
|
+
if (cached !== void 0 && cached.key === k) {
|
|
61
|
+
html = cached.html;
|
|
62
|
+
} else {
|
|
63
|
+
const out = render(item, i);
|
|
64
|
+
html = isSafeHtml(out) ? out.toString() : out;
|
|
65
|
+
ROW_CACHE.set(item, { key: k, html });
|
|
66
|
+
}
|
|
67
|
+
segItems[i] = { ref: item, cacheKey: k, html };
|
|
68
|
+
}
|
|
69
|
+
return listSafeHtml(id, segItems);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/diff.ts
|
|
46
73
|
var ID_KEY_PREFIX = "id:";
|
|
47
74
|
var DATA_KEY_PREFIX = "data-key:";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
var ELEMENT_NODE = 1;
|
|
76
|
+
var TEXT_NODE = 3;
|
|
77
|
+
var COMMENT_NODE = 8;
|
|
78
|
+
function getNodeKey(node) {
|
|
79
|
+
if (node.nodeType !== ELEMENT_NODE) return void 0;
|
|
80
|
+
const el = node;
|
|
81
|
+
if (el.id !== "") return `${ID_KEY_PREFIX}${el.id}`;
|
|
82
|
+
if (el.dataset !== void 0 && el.dataset.key !== void 0) {
|
|
83
|
+
return `${DATA_KEY_PREFIX}${el.dataset.key}`;
|
|
84
|
+
}
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
function diff(liveRoot, templateRoot, listParents) {
|
|
88
|
+
diffChildren(liveRoot, templateRoot, listParents);
|
|
89
|
+
}
|
|
90
|
+
function diffChildren(fromParent, toParent, listParents) {
|
|
91
|
+
const keyed = /* @__PURE__ */ new Map();
|
|
92
|
+
for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
|
|
93
|
+
const k = getNodeKey(c);
|
|
94
|
+
if (k !== void 0) keyed.set(k, c);
|
|
95
|
+
}
|
|
96
|
+
let fromChild = fromParent.firstChild;
|
|
97
|
+
let toChild = toParent.firstChild;
|
|
98
|
+
while (toChild !== null) {
|
|
99
|
+
const toNext = toChild.nextSibling;
|
|
100
|
+
let matched = null;
|
|
101
|
+
const toKey = getNodeKey(toChild);
|
|
102
|
+
if (toKey !== void 0 && keyed.has(toKey)) {
|
|
103
|
+
matched = keyed.get(toKey);
|
|
104
|
+
keyed.delete(toKey);
|
|
105
|
+
if (matched !== fromChild) {
|
|
106
|
+
fromParent.insertBefore(matched, fromChild);
|
|
107
|
+
} else {
|
|
108
|
+
fromChild = fromChild.nextSibling;
|
|
72
109
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
110
|
+
}
|
|
111
|
+
if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0)) {
|
|
112
|
+
matched = fromChild;
|
|
113
|
+
fromChild = fromChild.nextSibling;
|
|
114
|
+
}
|
|
115
|
+
if (matched !== null) {
|
|
116
|
+
morphNode(matched, toChild, listParents);
|
|
117
|
+
} else {
|
|
118
|
+
const cloned = toChild.cloneNode(true);
|
|
119
|
+
fromParent.insertBefore(cloned, fromChild);
|
|
120
|
+
}
|
|
121
|
+
toChild = toNext;
|
|
122
|
+
}
|
|
123
|
+
while (fromChild !== null) {
|
|
124
|
+
const next = fromChild.nextSibling;
|
|
125
|
+
fromParent.removeChild(fromChild);
|
|
126
|
+
fromChild = next;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function morphNode(fromNode, toNode, listParents) {
|
|
130
|
+
if (fromNode.nodeType === ELEMENT_NODE) {
|
|
131
|
+
morphElement(fromNode, toNode, listParents);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) {
|
|
135
|
+
const fromText = fromNode;
|
|
136
|
+
const toText = toNode;
|
|
137
|
+
if (fromText.data !== toText.data) fromText.data = toText.data;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function morphElement(fromEl, toEl, listParents) {
|
|
141
|
+
if (fromEl.tagName !== toEl.tagName) {
|
|
142
|
+
const replacement = toEl.cloneNode(true);
|
|
143
|
+
fromEl.parentNode?.replaceChild(replacement, fromEl);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (fromEl.dataset.morphSkip !== void 0) return;
|
|
147
|
+
if (fromEl.isEqualNode(toEl)) return;
|
|
148
|
+
if (fromEl === document.activeElement) {
|
|
149
|
+
const ce = fromEl.getAttribute("contenteditable");
|
|
150
|
+
if (ce !== null && ce.toLowerCase() !== "false") return;
|
|
151
|
+
if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
|
|
152
|
+
}
|
|
153
|
+
morphAttributes(fromEl, toEl);
|
|
154
|
+
if (listParents.has(fromEl)) return;
|
|
155
|
+
diffChildren(fromEl, toEl, listParents);
|
|
156
|
+
}
|
|
157
|
+
function morphAttributes(fromEl, toEl) {
|
|
158
|
+
const toAttrs = toEl.attributes;
|
|
159
|
+
for (let i = 0; i < toAttrs.length; i++) {
|
|
160
|
+
const attr = toAttrs[i];
|
|
161
|
+
const ns = attr.namespaceURI;
|
|
162
|
+
const name = attr.localName;
|
|
163
|
+
const value = attr.value;
|
|
164
|
+
if (ns !== null) {
|
|
165
|
+
if (fromEl.getAttributeNS(ns, name) !== value) {
|
|
166
|
+
fromEl.setAttributeNS(ns, attr.name, value);
|
|
167
|
+
}
|
|
168
|
+
} else if (fromEl.getAttribute(name) !== value) {
|
|
169
|
+
fromEl.setAttribute(name, value);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const fromAttrs = fromEl.attributes;
|
|
173
|
+
for (let i = fromAttrs.length - 1; i >= 0; i--) {
|
|
174
|
+
const attr = fromAttrs[i];
|
|
175
|
+
const ns = attr.namespaceURI;
|
|
176
|
+
const name = attr.localName;
|
|
177
|
+
if (ns !== null) {
|
|
178
|
+
if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);
|
|
179
|
+
} else if (!toEl.hasAttribute(name)) {
|
|
180
|
+
fromEl.removeAttribute(name);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
75
183
|
}
|
|
76
184
|
function isTextInputOrTextarea(el) {
|
|
77
185
|
if (el.tagName === "TEXTAREA") return true;
|
|
@@ -93,6 +201,168 @@ function preserveTextEntryState(fromEl, toEl) {
|
|
|
93
201
|
}
|
|
94
202
|
}
|
|
95
203
|
|
|
204
|
+
// src/mount.ts
|
|
205
|
+
var LIST_MARKER_PREFIX = "kf-list:";
|
|
206
|
+
function mount(rootEl, render) {
|
|
207
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
208
|
+
const counter = { value: 0 };
|
|
209
|
+
let isFirst = true;
|
|
210
|
+
return effect(() => {
|
|
211
|
+
counter.value = 0;
|
|
212
|
+
_setListCounter(counter);
|
|
213
|
+
let result;
|
|
214
|
+
try {
|
|
215
|
+
result = render();
|
|
216
|
+
} finally {
|
|
217
|
+
_setListCounter(null);
|
|
218
|
+
}
|
|
219
|
+
const segment = isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: result };
|
|
220
|
+
if (isFirst) {
|
|
221
|
+
rootEl.innerHTML = flatten(segment, true);
|
|
222
|
+
bindListsFromMarkers(rootEl, segment, bindings);
|
|
223
|
+
isFirst = false;
|
|
224
|
+
} else {
|
|
225
|
+
const template = rootEl.cloneNode(false);
|
|
226
|
+
template.innerHTML = flattenWithoutListItems(segment);
|
|
227
|
+
const listParents = /* @__PURE__ */ new Set();
|
|
228
|
+
for (const b of bindings.values()) listParents.add(b.liveParent);
|
|
229
|
+
diff(rootEl, template, listParents);
|
|
230
|
+
bindListsFromMarkers(rootEl, segment, bindings);
|
|
231
|
+
}
|
|
232
|
+
for (const listSeg of collectLists(segment).values()) {
|
|
233
|
+
const binding = bindings.get(listSeg.id);
|
|
234
|
+
reconcileList(binding, listSeg);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function bindListsFromMarkers(rootEl, segment, bindings) {
|
|
239
|
+
const lists = collectLists(segment);
|
|
240
|
+
const found = [];
|
|
241
|
+
collectComments(rootEl, found);
|
|
242
|
+
for (const marker of found) {
|
|
243
|
+
if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
|
|
244
|
+
const id = marker.data.slice(LIST_MARKER_PREFIX.length);
|
|
245
|
+
const listSeg = lists.get(id);
|
|
246
|
+
const liveParent = marker.parentElement;
|
|
247
|
+
const items = [];
|
|
248
|
+
let next = marker.nextElementSibling;
|
|
249
|
+
for (let i = 0; i < listSeg.items.length && next !== null; i++) {
|
|
250
|
+
items.push({
|
|
251
|
+
ref: listSeg.items[i].ref,
|
|
252
|
+
cacheKey: listSeg.items[i].cacheKey,
|
|
253
|
+
html: listSeg.items[i].html,
|
|
254
|
+
node: next
|
|
255
|
+
});
|
|
256
|
+
next = next.nextElementSibling;
|
|
257
|
+
}
|
|
258
|
+
bindings.set(id, { liveParent, items });
|
|
259
|
+
marker.remove();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function reconcileList(binding, listSeg) {
|
|
263
|
+
const { liveParent } = binding;
|
|
264
|
+
const oldItems = binding.items;
|
|
265
|
+
const oldByRef = /* @__PURE__ */ new Map();
|
|
266
|
+
const oldIndex = /* @__PURE__ */ new Map();
|
|
267
|
+
for (let i = 0; i < oldItems.length; i++) {
|
|
268
|
+
oldByRef.set(oldItems[i].ref, oldItems[i]);
|
|
269
|
+
oldIndex.set(oldItems[i].ref, i);
|
|
270
|
+
}
|
|
271
|
+
const newRecord = new Array(listSeg.items.length);
|
|
272
|
+
const prevIdx = new Array(listSeg.items.length);
|
|
273
|
+
const replacedNodes = [];
|
|
274
|
+
const freshIndices = [];
|
|
275
|
+
const freshHtmls = [];
|
|
276
|
+
for (let i = 0; i < listSeg.items.length; i++) {
|
|
277
|
+
const ni = listSeg.items[i];
|
|
278
|
+
const oi = oldByRef.get(ni.ref);
|
|
279
|
+
if (oi !== void 0) {
|
|
280
|
+
oldByRef.delete(ni.ref);
|
|
281
|
+
if (oi.html === ni.html) {
|
|
282
|
+
newRecord[i] = oi;
|
|
283
|
+
prevIdx[i] = oldIndex.get(ni.ref);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
replacedNodes.push(oi.node);
|
|
287
|
+
}
|
|
288
|
+
newRecord[i] = {
|
|
289
|
+
ref: ni.ref,
|
|
290
|
+
cacheKey: ni.cacheKey,
|
|
291
|
+
html: ni.html,
|
|
292
|
+
node: null
|
|
293
|
+
};
|
|
294
|
+
prevIdx[i] = -1;
|
|
295
|
+
freshIndices.push(i);
|
|
296
|
+
freshHtmls.push(ni.html);
|
|
297
|
+
}
|
|
298
|
+
if (freshHtmls.length > 0) {
|
|
299
|
+
const tpl = document.createElement("template");
|
|
300
|
+
tpl.innerHTML = freshHtmls.join("");
|
|
301
|
+
let node = tpl.content.firstElementChild;
|
|
302
|
+
for (const idx of freshIndices) {
|
|
303
|
+
if (node === null) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`each(): row render produced no top-level element. Each item's render must return exactly one element. Got HTML: ${newRecord[idx].html.slice(0, 120)}`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const next = node.nextElementSibling;
|
|
309
|
+
newRecord[idx].node = node;
|
|
310
|
+
node = next;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const orphan of oldByRef.values()) {
|
|
314
|
+
if (orphan.node.parentElement === liveParent) liveParent.removeChild(orphan.node);
|
|
315
|
+
}
|
|
316
|
+
for (const node of replacedNodes) {
|
|
317
|
+
if (node.parentElement === liveParent) liveParent.removeChild(node);
|
|
318
|
+
}
|
|
319
|
+
const stable = lis(prevIdx);
|
|
320
|
+
let nextSibling = null;
|
|
321
|
+
for (let i = newRecord.length - 1; i >= 0; i--) {
|
|
322
|
+
const node = newRecord[i].node;
|
|
323
|
+
if (prevIdx[i] !== -1 && stable.has(i)) ; else {
|
|
324
|
+
liveParent.insertBefore(node, nextSibling);
|
|
325
|
+
}
|
|
326
|
+
nextSibling = node;
|
|
327
|
+
}
|
|
328
|
+
binding.items = newRecord;
|
|
329
|
+
}
|
|
330
|
+
function lis(arr) {
|
|
331
|
+
const tails = [];
|
|
332
|
+
const tailIdx = [];
|
|
333
|
+
const prev = new Array(arr.length);
|
|
334
|
+
for (let i = 0; i < arr.length; i++) {
|
|
335
|
+
const v = arr[i];
|
|
336
|
+
if (v === -1) {
|
|
337
|
+
prev[i] = -1;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
let lo = 0;
|
|
341
|
+
let hi = tails.length;
|
|
342
|
+
while (lo < hi) {
|
|
343
|
+
const mid = lo + hi >> 1;
|
|
344
|
+
if (tails[mid] < v) lo = mid + 1;
|
|
345
|
+
else hi = mid;
|
|
346
|
+
}
|
|
347
|
+
prev[i] = lo > 0 ? tailIdx[lo - 1] : -1;
|
|
348
|
+
tails[lo] = v;
|
|
349
|
+
tailIdx[lo] = i;
|
|
350
|
+
}
|
|
351
|
+
const out = /* @__PURE__ */ new Set();
|
|
352
|
+
let k = tailIdx.length > 0 ? tailIdx[tailIdx.length - 1] : -1;
|
|
353
|
+
while (k !== -1) {
|
|
354
|
+
out.add(k);
|
|
355
|
+
k = prev[k];
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
function collectComments(node, out) {
|
|
360
|
+
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
|
|
361
|
+
if (c.nodeType === 8) out.push(c);
|
|
362
|
+
else if (c.nodeType === 1) collectComments(c, out);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
96
366
|
// src/toElement.ts
|
|
97
367
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
98
368
|
var SVG_FRAGMENT_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -160,6 +430,6 @@ function toElement(jsx) {
|
|
|
160
430
|
return child;
|
|
161
431
|
}
|
|
162
432
|
|
|
163
|
-
export { delegate, delegateCapture, mount, toElement };
|
|
433
|
+
export { delegate, delegateCapture, each, mount, toElement };
|
|
164
434
|
//# sourceMappingURL=index.js.map
|
|
165
435
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/delegate.ts","../src/mount.ts","../src/toElement.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Tiny event-delegation helpers. Replace per-element `addEventListener` calls\n * (which don't survive morph re-renders for nodes morphdom creates) with one\n * listener at the morph-root that dispatches via `closest()`.\n *\n * Three-tier listener model:\n *\n * - Tier 1 (bubbling events) — use `delegate()`.\n * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,\n * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.\n *\n * - Tier 2 (non-bubbling events) — use `delegateCapture()`.\n * focus, blur, scroll, load, error, mouseenter, mouseleave.\n * The capture phase fires on the way down from the root to the target,\n * so a root-level listener with `capture: true` reaches events that\n * wouldn't bubble back up.\n *\n * - Tier 3 (per-element instances / library-owned subtrees) — mark the\n * host element with `data-morph-skip` and manage the library's\n * lifecycle directly. No delegation helper applies.\n */\n\ntype Handler = (event: Event, target: Element) => void;\n\n/**\n * Validate a CSS selector at registration time, so a typo throws immediately\n * with the bad selector quoted instead of producing a cryptic DOMException\n * the first time a matching event fires.\n */\nfunction assertValidSelector(selector: string, fn: string): void {\n try {\n document.createElement('div').matches(selector);\n } catch {\n throw new Error(\n `${fn}: invalid selector \"${selector}\". `\n + 'Pass a valid CSS selector (e.g. \\'[data-action=\"add\"]\\', \\'.btn\\', \\'input\\').',\n );\n }\n}\n\n/**\n * Bubble-phase delegation. Installs ONE listener on `rootEl` for the given\n * event type. When the event fires, walks up from `event.target` to the root\n * looking for an element matching `selector`; if found, fires `handler` with\n * the matched element as the second arg.\n *\n * Returns a disposer that removes the listener.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegate(rootEl, 'click', '[data-action=\"add\"]', handlerFn);\n */\nexport function delegate(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: Handler,\n): () => void {\n assertValidSelector(selector, 'delegate');\n const listener = (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const matched = target.closest(selector);\n if (matched !== null && rootEl.contains(matched)) {\n handler(event, matched);\n }\n };\n rootEl.addEventListener(type, listener);\n return () => {\n rootEl.removeEventListener(type, listener);\n };\n}\n\n/**\n * Capture-phase delegation — for non-bubbling events (`focus`, `blur`,\n * `scroll`, `load`, `error`). Reaches descendants of `rootEl` that match\n * `selector` regardless of how many times morphdom has rebuilt them.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);\n */\nexport function delegateCapture(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: Handler,\n): () => void {\n assertValidSelector(selector, 'delegateCapture');\n const listener = (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n if (target.matches(selector) && rootEl.contains(target)) {\n handler(event, target);\n }\n };\n rootEl.addEventListener(type, listener, true);\n return () => {\n rootEl.removeEventListener(type, listener, true);\n };\n}\n","/**\n * `mount(rootEl, render)` — kerf's render primitive.\n *\n * Wraps `effect()` from `reactive.ts` so that whenever any signal read inside\n * `render()` changes, we re-run `render()` and use `morphdom` to apply the\n * minimal set of DOM mutations against the live tree. Element identity (and\n * thus focus, selection, in-flight pointer interactions, and event listeners\n * on preserved nodes) is preserved wherever the keyed/positional diff matches.\n *\n * Compared to a `replaceChildren(...rows.map(toElement))` rebuild pattern, the\n * user-visible win is that an `<input>` the user is typing into survives an\n * unrelated re-render — its DOM node, focus state, and cursor position are\n * not destroyed and recreated on each tick.\n */\n\nimport morphdom from 'morphdom';\n\nimport type { SafeHtml } from './jsx-runtime.js';\nimport { isSafeHtml } from './jsx-runtime.js';\nimport { effect } from './reactive.js';\n\n// Distinct namespaces inside morphdom's flat string-keyed match space, so a\n// consumer with `id=\"foo\"` and a sibling with `data-key=\"foo\"` cannot collide.\n// The prefixes also can't collide with each other across consumer values\n// (e.g. `id=\"data-key:foo\"` vs `data-key=\"foo\"` would have produced the same\n// key under a single-prefix scheme; here they don't).\nconst ID_KEY_PREFIX = 'id:';\nconst DATA_KEY_PREFIX = 'data-key:';\n\n/**\n * Bind `render()` to the children of `rootEl`. Re-runs whenever any signal\n * read inside `render()` changes. Returns a disposer that tears down the\n * effect; call it when the host element is removed from the DOM.\n *\n * Conventions:\n *\n * - Diff keys: `id` and `data-key` are matched across the morph by key\n * rather than positionally, so list reorders move existing nodes instead\n * of churning unrelated siblings.\n * - `data-morph-skip`: any element with this attribute is left untouched\n * inside on subsequent renders. Used for library-owned subtrees (xterm-\n * style widgets, charts, third-party editors) where the library's own\n * lifecycle manages the children.\n * - Focused text-entry inputs (`<input>` of typing kinds, `<textarea>`)\n * keep their current value + selection range across morphs while focused.\n * The user never sees their cursor jump mid-keystroke.\n * - Focused `[contenteditable]` elements have their entire subtree\n * skipped (same mechanism as `data-morph-skip`). The user's in-progress\n * edit — typed content, caret position, multi-range selections, anything\n * else they did to the DOM — survives verbatim. The next render after\n * blur catches up.\n */\nexport function mount(rootEl: HTMLElement, render: () => SafeHtml | string): () => void {\n return effect(() => {\n const next = render();\n const html = isSafeHtml(next) ? next.toString() : next;\n\n const template = rootEl.cloneNode(false) as HTMLElement;\n template.innerHTML = html;\n\n morphdom(rootEl, template, {\n childrenOnly: true,\n getNodeKey: (node) => {\n if (node.nodeType !== 1) return undefined;\n const el = node as HTMLElement;\n if (el.id !== '') return `${ID_KEY_PREFIX}${el.id}`;\n if (el.dataset.key != null) return `${DATA_KEY_PREFIX}${el.dataset.key}`;\n return undefined;\n },\n onBeforeElUpdated: (fromEl, toEl) => {\n if (fromEl.dataset.morphSkip != null) return false;\n if (fromEl.isEqualNode(toEl)) return false;\n if (fromEl === document.activeElement) {\n // Focused contenteditable: skip the entire subtree so the user's\n // in-progress edit (typed content + caret + multi-range selection)\n // is not disturbed. A subsequent render — typically after blur or\n // an explicit signal write — catches up. We read the attribute\n // directly rather than the derived `isContentEditable` property\n // because happy-dom (test environment) doesn't always populate\n // the latter; the attribute is the spec's source of truth either\n // way. Per the HTML spec, any value other than `\"false\"` (case-\n // insensitive), including the empty string, means editable.\n const ce = fromEl.getAttribute('contenteditable');\n if (ce !== null && ce.toLowerCase() !== 'false') return false;\n // Focused INPUT / TEXTAREA: copy live value + selection onto the\n // morph target before letting morphdom proceed.\n if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);\n }\n return true;\n },\n });\n });\n}\n\nfunction isTextInputOrTextarea(el: Element): boolean {\n if (el.tagName === 'TEXTAREA') return true;\n if (el.tagName === 'INPUT') {\n const type = (el as HTMLInputElement).type;\n return type === 'text' || type === 'search' || type === 'url' || type === 'email'\n || type === 'tel' || type === 'password' || type === '';\n }\n return false;\n}\n\nfunction preserveTextEntryState(fromEl: HTMLElement, toEl: HTMLElement): void {\n if (fromEl.tagName === 'TEXTAREA' || fromEl.tagName === 'INPUT') {\n const fromInput = fromEl as HTMLInputElement;\n const toInput = toEl as HTMLInputElement;\n toInput.value = fromInput.value;\n try {\n toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd);\n } catch {\n // Some input types (number, range, color, …) reject selection APIs.\n }\n }\n}\n","/**\n * `toElement(jsx)` — JSX → DOM, with SVG-aware namespace handling.\n *\n * The naive implementation parses JSX through a `<template>` element's\n * `innerHTML`. That works for HTML and for SVG fragments whose root tag is\n * `<svg>` (the parser switches to \"foreign content\" mode). It silently\n * fails for SVG fragments WITHOUT an `<svg>` wrapper — descendants come out\n * as `HTMLUnknownElement` and never paint.\n *\n * `toElement` detects SVG content and routes through `DOMParser` with the\n * `image/svg+xml` MIME, which guarantees correct namespacing for all\n * descendants. HTML content takes the original `<template>` path unchanged.\n */\n\nimport type { SafeHtml } from './jsx-runtime.js';\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\nconst SVG_FRAGMENT_TAGS = new Set([\n 'g', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse',\n 'text', 'tspan', 'defs', 'use', 'symbol', 'clipPath', 'mask', 'pattern',\n 'filter', 'marker', 'linearGradient', 'radialGradient', 'stop', 'image',\n 'foreignObject',\n]);\n\nconst EXCERPT_MAX_LEN = 100;\n\nfunction leadingTag(html: string): string | null {\n const match = /^\\s*<([a-zA-Z][a-zA-Z0-9]*)\\b/.exec(html);\n return match !== null ? match[1] : null;\n}\n\nfunction excerpt(html: string): string {\n const trimmed = html.trim();\n return trimmed.length > EXCERPT_MAX_LEN ? `${trimmed.slice(0, EXCERPT_MAX_LEN)}…` : trimmed;\n}\n\nfunction parseSvgOrThrow(html: string, label: string, originalHtml: string): Document {\n const doc = new DOMParser().parseFromString(html, 'image/svg+xml');\n const err = doc.querySelector('parsererror');\n if (err !== null) {\n throw new Error(`toElement: ${label} parse error — ${err.textContent}\\n input: ${excerpt(originalHtml)}`);\n }\n return doc;\n}\n\nexport function toElement(jsx: SafeHtml | string): Element {\n const html = typeof jsx === 'string' ? jsx : jsx.toString();\n const tag = leadingTag(html);\n\n if (tag === 'svg') {\n // SVG root — parse as XML to guarantee namespace propagation.\n return parseSvgOrThrow(html, 'SVG', html).documentElement;\n }\n\n if (tag !== null && SVG_FRAGMENT_TAGS.has(tag)) {\n // SVG fragment without an <svg> wrapper — wrap, parse, unwrap.\n const wrapped = `<svg xmlns=\"${SVG_NS}\">${html}</svg>`;\n const doc = parseSvgOrThrow(wrapped, 'SVG fragment', html);\n const first = doc.documentElement.firstElementChild;\n /* c8 ignore next 2 — defensive: a successful XML parse of a wrapped svg always yields ≥1 child. */\n if (first === null) throw new Error(`toElement: SVG fragment produced no element\\n input: ${excerpt(html)}`);\n return first;\n }\n\n // HTML — `<template>`-based parse.\n const t = document.createElement('template');\n t.innerHTML = html;\n const child = t.content.firstElementChild;\n if (child === null) throw new Error(`toElement: produced no element\\n input: ${excerpt(html)}`);\n return child;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/delegate.ts","../src/each.ts","../src/diff.ts","../src/mount.ts","../src/toElement.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Tiny event-delegation helpers. Replace per-element `addEventListener` calls\n * (which don't survive morph re-renders for nodes morphdom creates) with one\n * listener at the morph-root that dispatches via `closest()`.\n *\n * Three-tier listener model:\n *\n * - Tier 1 (bubbling events) — use `delegate()`.\n * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,\n * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.\n *\n * - Tier 2 (non-bubbling events) — use `delegateCapture()`.\n * focus, blur, scroll, load, error, mouseenter, mouseleave.\n * The capture phase fires on the way down from the root to the target,\n * so a root-level listener with `capture: true` reaches events that\n * wouldn't bubble back up.\n *\n * - Tier 3 (per-element instances / library-owned subtrees) — mark the\n * host element with `data-morph-skip` and manage the library's\n * lifecycle directly. No delegation helper applies.\n */\n\ntype Handler = (event: Event, target: Element) => void;\n\n/**\n * Validate a CSS selector at registration time, so a typo throws immediately\n * with the bad selector quoted instead of producing a cryptic DOMException\n * the first time a matching event fires.\n */\nfunction assertValidSelector(selector: string, fn: string): void {\n try {\n document.createElement('div').matches(selector);\n } catch {\n throw new Error(\n `${fn}: invalid selector \"${selector}\". `\n + 'Pass a valid CSS selector (e.g. \\'[data-action=\"add\"]\\', \\'.btn\\', \\'input\\').',\n );\n }\n}\n\n/**\n * Bubble-phase delegation. Installs ONE listener on `rootEl` for the given\n * event type. When the event fires, walks up from `event.target` to the root\n * looking for an element matching `selector`; if found, fires `handler` with\n * the matched element as the second arg.\n *\n * Returns a disposer that removes the listener.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegate(rootEl, 'click', '[data-action=\"add\"]', handlerFn);\n */\nexport function delegate(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: Handler,\n): () => void {\n assertValidSelector(selector, 'delegate');\n const listener = (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const matched = target.closest(selector);\n if (matched !== null && rootEl.contains(matched)) {\n handler(event, matched);\n }\n };\n rootEl.addEventListener(type, listener);\n return () => {\n rootEl.removeEventListener(type, listener);\n };\n}\n\n/**\n * Capture-phase delegation — for non-bubbling events (`focus`, `blur`,\n * `scroll`, `load`, `error`). Reaches descendants of `rootEl` that match\n * `selector` regardless of how many times morphdom has rebuilt them.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);\n */\nexport function delegateCapture(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: Handler,\n): () => void {\n assertValidSelector(selector, 'delegateCapture');\n const listener = (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n if (target.matches(selector) && rootEl.contains(target)) {\n handler(event, target);\n }\n };\n rootEl.addEventListener(type, listener, true);\n return () => {\n rootEl.removeEventListener(type, listener, true);\n };\n}\n","/**\n * `each(items, render, key?)` — keyed list iteration with per-item memoisation.\n *\n * Drops in as the body of a list-rendering JSX expression inside a `mount()`\n * render function. Returns a `SafeHtml` carrying a structured list segment,\n * so `mount()` can run a native keyed reconciler instead of the general-\n * purpose morph for these children.\n *\n * Two layers of optimisation:\n *\n * 1. Per-item memoisation. `render(item)` is skipped for items whose object\n * identity (and optional `key`) are unchanged since the previous call.\n * Their HTML strings come from a `WeakMap` keyed by item reference. The\n * immutable-update style (\"replace the row object\" instead of \"mutate it\")\n * makes the cache work automatically.\n *\n * 2. Structural handoff. `mount()` recognises the list segment and bypasses\n * the parse-the-whole-table round trip: only fresh items get parsed (one\n * at a time, into the smallest detached element), and only changed rows\n * get patched in the live DOM. Unchanged rows are physically the same\n * nodes they were before — never visited.\n *\n * `key` covers the case where external state, not the item itself, drives\n * what the row should render (e.g. a \"currently selected\" id flips a CSS\n * class on one row). Same item identity but a different `key` value means\n * \"re-render this item.\" If you don't pass `key`, only identity changes\n * invalidate.\n *\n * Items must be objects (cache is a `WeakMap`); wrap primitives if you need\n * to iterate them. Each item's render output must produce exactly one\n * top-level element — the list reconciler binds one live DOM node per item.\n */\n\nimport type { SafeHtml } from './jsx-runtime.js';\nimport { isSafeHtml, listSafeHtml } from './jsx-runtime.js';\n\ninterface CacheEntry {\n key: unknown;\n html: string;\n}\n\nconst ROW_CACHE = new WeakMap<object, CacheEntry>();\n\n/**\n * Per-mount counter for assigning stable list ids across renders. `mount()`\n * sets this at the start of each render so that the n-th `each()` call\n * produces id \"n\" every render — the binding to the live parent persists.\n *\n * Outside a mount() render, calls to `each()` still work but get the\n * sentinel id \"orphan\"; their output flattens correctly via `toString()`\n * but the structural fast-path doesn't apply (no `mount()` is watching).\n */\nlet listCounter: { value: number } | null = null;\n\nexport function _setListCounter(c: { value: number } | null): void {\n listCounter = c;\n}\n\nexport function each<T extends object>(\n items: readonly T[],\n render: (item: T, index: number) => SafeHtml | string,\n key?: (item: T, index: number) => unknown,\n): SafeHtml {\n const id = listCounter !== null ? String(listCounter.value++) : 'orphan';\n const segItems = new Array<{ ref: object; cacheKey: unknown; html: string }>(items.length);\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n const k = key ? key(item, i) : undefined;\n const cached = ROW_CACHE.get(item);\n let html: string;\n if (cached !== undefined && cached.key === k) {\n html = cached.html;\n } else {\n const out = render(item, i);\n html = isSafeHtml(out) ? out.toString() : out;\n ROW_CACHE.set(item, { key: k, html });\n }\n segItems[i] = { ref: item, cacheKey: k, html };\n }\n return listSafeHtml(id, segItems);\n}\n","/**\n * `diff(liveRoot, templateRoot, listParents)` — minimum-mutation DOM\n * reconciliation.\n *\n * Replaces our previous dependency on `morphdom`. The algorithm is the\n * classic two-tree walk: match children by key (id, then data-key, then\n * positional same-tag), morph matches in place, insert / remove / clone\n * the rest. Specialised for what kerf needs:\n *\n * - `childrenOnly` is always true; the live root is never replaced.\n * - Three short-circuit paths on each element:\n * 1. `data-morph-skip`: subtree is library-owned, leave verbatim.\n * 2. `isEqualNode`: subtree is byte-identical, no work needed.\n * 3. `listParents` membership: a kerf list reconciler owns this\n * element's children, so we morph attributes only and stop.\n * - Focused text inputs (`<input>`/`<textarea>`) keep their value +\n * selection across the morph; focused `[contenteditable]` keeps its\n * entire subtree (typed content + caret + multi-range selection).\n *\n * Algorithm credit: based on the design of\n * https://github.com/patrick-steele-idem/morphdom by Patrick Steele-Idem\n * (MIT licensed). Reimplemented here so kerf can specialise the hot paths\n * (segment-aware list dispatch, lighter callback surface) and drop the\n * runtime dependency. Original copyright preserved in `LICENSE`.\n */\n\nconst ID_KEY_PREFIX = 'id:';\nconst DATA_KEY_PREFIX = 'data-key:';\nconst ELEMENT_NODE = 1;\nconst TEXT_NODE = 3;\nconst COMMENT_NODE = 8;\n\nfunction getNodeKey(node: Node): string | undefined {\n if (node.nodeType !== ELEMENT_NODE) return undefined;\n const el = node as HTMLElement;\n if (el.id !== '') return `${ID_KEY_PREFIX}${el.id}`;\n if (el.dataset !== undefined && el.dataset.key !== undefined) {\n return `${DATA_KEY_PREFIX}${el.dataset.key}`;\n }\n return undefined;\n}\n\n/**\n * Reconcile the children of `liveRoot` to match the children of\n * `templateRoot`. `listParents` is the set of live elements whose children\n * are managed by `each()`'s reconciler — they get attribute morphing but\n * not children diffing.\n */\nexport function diff(\n liveRoot: Element,\n templateRoot: Element,\n listParents: ReadonlySet<Element>,\n): void {\n diffChildren(liveRoot, templateRoot, listParents);\n}\n\nfunction diffChildren(\n fromParent: Element,\n toParent: Element,\n listParents: ReadonlySet<Element>,\n): void {\n // Build a keyed lookup from the live children so we can match by key\n // even after reorders.\n const keyed = new Map<string, Element>();\n for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {\n const k = getNodeKey(c);\n if (k !== undefined) keyed.set(k, c as Element);\n }\n\n let fromChild: Node | null = fromParent.firstChild;\n let toChild: Node | null = toParent.firstChild;\n\n while (toChild !== null) {\n const toNext = toChild.nextSibling;\n let matched: Node | null = null;\n\n // 1. Try key match.\n const toKey = getNodeKey(toChild);\n if (toKey !== undefined && keyed.has(toKey)) {\n matched = keyed.get(toKey) as Element;\n keyed.delete(toKey);\n if (matched !== fromChild) {\n fromParent.insertBefore(matched, fromChild);\n } else {\n fromChild = fromChild.nextSibling;\n }\n }\n\n // 2. Fall back to positional match — same nodeType, same tag (for\n // elements), and the live node has no key (a keyed node only\n // matches by key, never positionally, so reorders work).\n if (matched === null && fromChild !== null\n && fromChild.nodeType === toChild.nodeType\n && (toChild.nodeType !== ELEMENT_NODE\n || ((fromChild as Element).tagName === (toChild as Element).tagName\n && getNodeKey(fromChild) === undefined))) {\n matched = fromChild;\n fromChild = fromChild.nextSibling;\n }\n\n if (matched !== null) {\n morphNode(matched, toChild, listParents);\n } else {\n // 3. No match — clone the new node and insert.\n const cloned = toChild.cloneNode(true);\n fromParent.insertBefore(cloned, fromChild);\n }\n\n toChild = toNext;\n }\n\n // 4. Anything past the cursor is unmatched — remove. Any keyed node that\n // wasn't matched in the toParent walk falls into this trailing range\n // by construction (matched keyed nodes get moved before the cursor;\n // unmatched ones stay at or after it), so we don't need a separate\n // orphan pass.\n while (fromChild !== null) {\n const next = fromChild.nextSibling;\n fromParent.removeChild(fromChild);\n fromChild = next;\n }\n}\n\nfunction morphNode(\n fromNode: Node,\n toNode: Node,\n listParents: ReadonlySet<Element>,\n): void {\n if (fromNode.nodeType === ELEMENT_NODE) {\n morphElement(fromNode as Element, toNode as Element, listParents);\n return;\n }\n if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) {\n const fromText = fromNode as CharacterData;\n const toText = toNode as CharacterData;\n if (fromText.data !== toText.data) fromText.data = toText.data;\n }\n}\n\nfunction morphElement(\n fromEl: Element,\n toEl: Element,\n listParents: ReadonlySet<Element>,\n): void {\n if (fromEl.tagName !== toEl.tagName) {\n const replacement = toEl.cloneNode(true);\n fromEl.parentNode?.replaceChild(replacement, fromEl);\n return;\n }\n // 1. Library-owned subtree — leave verbatim.\n if ((fromEl as HTMLElement).dataset.morphSkip !== undefined) return;\n // 2. Byte-identical — nothing to do.\n if (fromEl.isEqualNode(toEl)) return;\n // 3. Focused contenteditable — preserve user's in-progress edit.\n if (fromEl === document.activeElement) {\n const ce = fromEl.getAttribute('contenteditable');\n if (ce !== null && ce.toLowerCase() !== 'false') return;\n if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);\n }\n morphAttributes(fromEl, toEl);\n // 4. List parent — its children are managed by the each() reconciler;\n // diff stops here. We still ran morphAttributes above, so attribute\n // changes on the parent itself (id, class, data-* …) propagate.\n if (listParents.has(fromEl)) return;\n diffChildren(fromEl, toEl, listParents);\n}\n\nfunction morphAttributes(fromEl: Element, toEl: Element): void {\n // Set/update every attribute on toEl.\n const toAttrs = toEl.attributes;\n for (let i = 0; i < toAttrs.length; i++) {\n const attr = toAttrs[i];\n const ns = attr.namespaceURI;\n const name = attr.localName;\n const value = attr.value;\n if (ns !== null) {\n if (fromEl.getAttributeNS(ns, name) !== value) {\n fromEl.setAttributeNS(ns, attr.name, value);\n }\n } else if (fromEl.getAttribute(name) !== value) {\n fromEl.setAttribute(name, value);\n }\n }\n // Remove attributes that are no longer present on toEl.\n const fromAttrs = fromEl.attributes;\n for (let i = fromAttrs.length - 1; i >= 0; i--) {\n const attr = fromAttrs[i];\n const ns = attr.namespaceURI;\n const name = attr.localName;\n if (ns !== null) {\n if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);\n } else if (!toEl.hasAttribute(name)) {\n fromEl.removeAttribute(name);\n }\n }\n}\n\nfunction isTextInputOrTextarea(el: Element): boolean {\n if (el.tagName === 'TEXTAREA') return true;\n if (el.tagName === 'INPUT') {\n const type = (el as HTMLInputElement).type;\n return type === 'text' || type === 'search' || type === 'url' || type === 'email'\n || type === 'tel' || type === 'password' || type === '';\n }\n return false;\n}\n\nfunction preserveTextEntryState(fromEl: Element, toEl: Element): void {\n if (fromEl.tagName === 'TEXTAREA' || fromEl.tagName === 'INPUT') {\n const fromInput = fromEl as HTMLInputElement;\n const toInput = toEl as HTMLInputElement;\n toInput.value = fromInput.value;\n try {\n toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd);\n } catch {\n // Some input types (number, range, color, …) reject selection APIs.\n }\n }\n}\n","/**\n * `mount(rootEl, render)` — kerf's render primitive.\n *\n * Wraps `effect()` so that whenever any signal read inside `render()`\n * changes, we re-run `render()` and apply the minimum DOM mutations against\n * the live tree. Element identity (and thus focus, selection, in-flight\n * pointer interactions, and event listeners on preserved nodes) is preserved\n * wherever the keyed/positional diff matches.\n *\n * Two phases per render:\n *\n * - Static surrounds (everything outside `each()` lists): morphdom diffs\n * a freshly-built template against the live tree. Same conventions as\n * before — id/data-key matching, `data-morph-skip`, focus preservation.\n *\n * - List interiors (children of every `each()` parent): native keyed\n * reconciler operates directly on the live parent's children. No\n * re-parse, no morph walk for cache-hit rows. Cost is O(changes), not\n * O(rows).\n *\n * Compared to a `replaceChildren(...rows.map(toElement))` rebuild pattern,\n * the user-visible win is that an `<input>` the user is typing into\n * survives an unrelated re-render — its DOM node, focus state, and cursor\n * position are not destroyed and recreated on each tick.\n */\n\nimport { diff } from './diff.js';\nimport { _setListCounter } from './each.js';\nimport type { SafeHtml } from './jsx-runtime.js';\nimport { isSafeHtml } from './jsx-runtime.js';\nimport { effect } from './reactive.js';\nimport {\n collectLists,\n flatten,\n flattenWithoutListItems,\n type ListSegment,\n type Segment,\n} from './segment.js';\n\ninterface BoundItem {\n ref: object;\n cacheKey: unknown;\n html: string;\n node: Element;\n}\n\ninterface ListBinding {\n liveParent: Element;\n /**\n * One entry per item currently mounted under `liveParent`, in order.\n * Mirrors the current segment's `items` length after each reconcile.\n */\n items: BoundItem[];\n}\n\nconst LIST_MARKER_PREFIX = 'kf-list:';\n\n/**\n * Bind `render()` to the children of `rootEl`. Re-runs whenever any signal\n * read inside `render()` changes. Returns a disposer that tears down the\n * effect; call it when the host element is removed from the DOM.\n *\n * Conventions:\n *\n * - Diff keys: `id` and `data-key` are matched across the morph by key\n * rather than positionally, so list reorders move existing nodes instead\n * of churning unrelated siblings.\n * - `data-morph-skip`: any element with this attribute is left untouched\n * inside on subsequent renders. Used for library-owned subtrees (xterm-\n * style widgets, charts, third-party editors) where the library's own\n * lifecycle manages the children.\n * - Focused text-entry inputs (`<input>` of typing kinds, `<textarea>`)\n * keep their current value + selection range across morphs while focused.\n * The user never sees their cursor jump mid-keystroke.\n * - Focused `[contenteditable]` elements have their entire subtree\n * skipped (same mechanism as `data-morph-skip`). The user's in-progress\n * edit — typed content, caret position, multi-range selections, anything\n * else they did to the DOM — survives verbatim. The next render after\n * blur catches up.\n */\nexport function mount(rootEl: HTMLElement, render: () => SafeHtml | string): () => void {\n const bindings = new Map<string, ListBinding>();\n const counter = { value: 0 };\n let isFirst = true;\n\n return effect(() => {\n counter.value = 0;\n _setListCounter(counter);\n let result: SafeHtml | string;\n try {\n result = render();\n } finally {\n _setListCounter(null);\n }\n\n const segment: Segment = isSafeHtml(result)\n ? (result.__segment ?? { kind: 'static', html: result.__html })\n : { kind: 'static', html: result };\n\n if (isFirst) {\n // Bulk-render with items inlined and a marker per list. The marker walk\n // afterwards binds each list to the rows already in the DOM, so the\n // first-render reconcile is a no-op (every item is a cache hit).\n rootEl.innerHTML = flatten(segment, true);\n bindListsFromMarkers(rootEl, segment, bindings);\n isFirst = false;\n } else {\n // Static-surrounds-only render: lists become marker-only in the\n // template. `diff()` skips bound list parents' children entirely\n // (so existing rows stay) and inserts the marker for any list that\n // didn't exist before. The marker walk afterwards binds those new\n // lists; the per-list reconcile below patches every list's items.\n const template = rootEl.cloneNode(false) as HTMLElement;\n template.innerHTML = flattenWithoutListItems(segment);\n const listParents = new Set<Element>();\n for (const b of bindings.values()) listParents.add(b.liveParent);\n diff(rootEl, template, listParents);\n bindListsFromMarkers(rootEl, segment, bindings);\n }\n\n for (const listSeg of collectLists(segment).values()) {\n const binding = bindings.get(listSeg.id) as ListBinding;\n reconcileList(binding, listSeg);\n }\n });\n}\n\n/**\n * Walk the live tree's comment nodes; every `<!--kf-list:{id}-->` marker\n * is the first child of a list parent. Record the binding (parent + the\n * already-rendered item nodes that follow the marker) and then remove the\n * marker — bindings live in JS, not in the DOM.\n */\nfunction bindListsFromMarkers(\n rootEl: Element,\n segment: Segment,\n bindings: Map<string, ListBinding>,\n): void {\n const lists = collectLists(segment);\n const found: Comment[] = [];\n collectComments(rootEl, found);\n for (const marker of found) {\n if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;\n const id = marker.data.slice(LIST_MARKER_PREFIX.length);\n // Markers are only emitted by `flatten(..., true)` paired with a list\n // segment in the same render, so both lookups always succeed here.\n const listSeg = lists.get(id) as ListSegment;\n const liveParent = marker.parentElement as Element;\n // On first render, items are inlined right after the marker (so the\n // first element sibling is `items[0]`, the next is `items[1]`, etc.).\n // On subsequent renders for a list newly appearing, the list parent\n // is empty after the marker; the loop falls through with `items=[]`\n // and the reconcile phase below builds the items.\n const items: BoundItem[] = [];\n let next: Element | null = marker.nextElementSibling;\n for (let i = 0; i < listSeg.items.length && next !== null; i++) {\n items.push({\n ref: listSeg.items[i].ref,\n cacheKey: listSeg.items[i].cacheKey,\n html: listSeg.items[i].html,\n node: next,\n });\n next = next.nextElementSibling;\n }\n bindings.set(id, { liveParent, items });\n marker.remove();\n }\n}\n\nfunction reconcileList(binding: ListBinding, listSeg: ListSegment): void {\n const { liveParent } = binding;\n const oldItems = binding.items;\n const oldByRef = new Map<object, BoundItem>();\n const oldIndex = new Map<object, number>();\n for (let i = 0; i < oldItems.length; i++) {\n oldByRef.set(oldItems[i].ref, oldItems[i]);\n oldIndex.set(oldItems[i].ref, i);\n }\n\n // Build the new record. Per item, decide:\n // - \"stable\": cache hit (same ref, byte-identical html) — reuse the\n // existing live node. Captures its old position in `prevIdx` so the\n // LIS pass can decide whether the node also needs to move.\n // - \"replaced\": same ref but html changed — schedule a fresh node;\n // the old node will be removed before the move pass.\n // - \"new\": ref didn't exist before — schedule a fresh node.\n //\n // Fresh nodes aren't built one-at-a-time; instead we collect every fresh\n // item's HTML and parse them all in one `innerHTML` call below. For an\n // initial population of, say, 10k rows that's 1 parse instead of 10k.\n const newRecord: BoundItem[] = new Array(listSeg.items.length);\n const prevIdx = new Array<number>(listSeg.items.length);\n const replacedNodes: Element[] = [];\n const freshIndices: number[] = [];\n const freshHtmls: string[] = [];\n\n for (let i = 0; i < listSeg.items.length; i++) {\n const ni = listSeg.items[i];\n const oi = oldByRef.get(ni.ref);\n if (oi !== undefined) {\n oldByRef.delete(ni.ref);\n if (oi.html === ni.html) {\n newRecord[i] = oi;\n prevIdx[i] = oldIndex.get(ni.ref) as number;\n continue;\n }\n replacedNodes.push(oi.node);\n }\n // Placeholder; node is filled in by the bulk parse below.\n newRecord[i] = {\n ref: ni.ref, cacheKey: ni.cacheKey, html: ni.html, node: null as unknown as Element,\n };\n prevIdx[i] = -1;\n freshIndices.push(i);\n freshHtmls.push(ni.html);\n }\n\n if (freshHtmls.length > 0) {\n const tpl = document.createElement('template');\n tpl.innerHTML = freshHtmls.join('');\n let node = tpl.content.firstElementChild;\n for (const idx of freshIndices) {\n if (node === null) {\n throw new Error(\n `each(): row render produced no top-level element. Each item's render must return exactly one element. Got HTML: ${newRecord[idx].html.slice(0, 120)}`,\n );\n }\n const next = node.nextElementSibling;\n newRecord[idx].node = node;\n node = next;\n }\n }\n\n // Remove orphans (refs that disappeared from the new list) and the old\n // nodes for replaced items. Both are out of `oldByRef` already; replaced\n // ones were captured separately.\n for (const orphan of oldByRef.values()) {\n if (orphan.node.parentElement === liveParent) liveParent.removeChild(orphan.node);\n }\n for (const node of replacedNodes) {\n if (node.parentElement === liveParent) liveParent.removeChild(node);\n }\n\n // Compute the longest increasing subsequence of `prevIdx`. New positions\n // whose index is in the LIS are already in the right relative order — we\n // skip moving them. Everything else (replaced, new, or relatively-out-of-\n // order stable items) gets `insertBefore`d in a single reverse pass.\n const stable = lis(prevIdx);\n let nextSibling: Element | null = null;\n for (let i = newRecord.length - 1; i >= 0; i--) {\n const node = newRecord[i].node;\n if (prevIdx[i] !== -1 && stable.has(i)) {\n // In LIS — already in the right place.\n } else {\n liveParent.insertBefore(node, nextSibling);\n }\n nextSibling = node;\n }\n\n binding.items = newRecord;\n}\n\n/**\n * Patience-sort LIS: returns the *set of indices* in `arr` that participate\n * in a longest strictly-increasing subsequence. `-1` entries (representing\n * brand-new items in the new list) are ignored — they can't anchor the\n * subsequence and shouldn't count as stable.\n */\nfunction lis(arr: ReadonlyArray<number>): ReadonlySet<number> {\n const tails: number[] = [];\n const tailIdx: number[] = [];\n const prev = new Array<number>(arr.length);\n for (let i = 0; i < arr.length; i++) {\n const v = arr[i];\n if (v === -1) {\n prev[i] = -1;\n continue;\n }\n let lo = 0;\n let hi = tails.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (tails[mid] < v) lo = mid + 1;\n else hi = mid;\n }\n prev[i] = lo > 0 ? tailIdx[lo - 1] : -1;\n tails[lo] = v;\n tailIdx[lo] = i;\n }\n const out = new Set<number>();\n let k = tailIdx.length > 0 ? tailIdx[tailIdx.length - 1] : -1;\n while (k !== -1) {\n out.add(k);\n k = prev[k];\n }\n return out;\n}\n\n/**\n * Recursive collector for comment nodes — happy-dom's `TreeWalker` doesn't\n * surface `Node.COMMENT_NODE` despite accepting `NodeFilter.SHOW_COMMENT`,\n * so we walk children directly. Cheap (O(elements)) and portable.\n */\nfunction collectComments(node: Node, out: Comment[]): void {\n for (let c: Node | null = node.firstChild; c !== null; c = c.nextSibling) {\n if (c.nodeType === 8) out.push(c as Comment);\n else if (c.nodeType === 1) collectComments(c, out);\n }\n}\n\n\n","/**\n * `toElement(jsx)` — JSX → DOM, with SVG-aware namespace handling.\n *\n * The naive implementation parses JSX through a `<template>` element's\n * `innerHTML`. That works for HTML and for SVG fragments whose root tag is\n * `<svg>` (the parser switches to \"foreign content\" mode). It silently\n * fails for SVG fragments WITHOUT an `<svg>` wrapper — descendants come out\n * as `HTMLUnknownElement` and never paint.\n *\n * `toElement` detects SVG content and routes through `DOMParser` with the\n * `image/svg+xml` MIME, which guarantees correct namespacing for all\n * descendants. HTML content takes the original `<template>` path unchanged.\n */\n\nimport type { SafeHtml } from './jsx-runtime.js';\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\nconst SVG_FRAGMENT_TAGS = new Set([\n 'g', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse',\n 'text', 'tspan', 'defs', 'use', 'symbol', 'clipPath', 'mask', 'pattern',\n 'filter', 'marker', 'linearGradient', 'radialGradient', 'stop', 'image',\n 'foreignObject',\n]);\n\nconst EXCERPT_MAX_LEN = 100;\n\nfunction leadingTag(html: string): string | null {\n const match = /^\\s*<([a-zA-Z][a-zA-Z0-9]*)\\b/.exec(html);\n return match !== null ? match[1] : null;\n}\n\nfunction excerpt(html: string): string {\n const trimmed = html.trim();\n return trimmed.length > EXCERPT_MAX_LEN ? `${trimmed.slice(0, EXCERPT_MAX_LEN)}…` : trimmed;\n}\n\nfunction parseSvgOrThrow(html: string, label: string, originalHtml: string): Document {\n const doc = new DOMParser().parseFromString(html, 'image/svg+xml');\n const err = doc.querySelector('parsererror');\n if (err !== null) {\n throw new Error(`toElement: ${label} parse error — ${err.textContent}\\n input: ${excerpt(originalHtml)}`);\n }\n return doc;\n}\n\nexport function toElement(jsx: SafeHtml | string): Element {\n const html = typeof jsx === 'string' ? jsx : jsx.toString();\n const tag = leadingTag(html);\n\n if (tag === 'svg') {\n // SVG root — parse as XML to guarantee namespace propagation.\n return parseSvgOrThrow(html, 'SVG', html).documentElement;\n }\n\n if (tag !== null && SVG_FRAGMENT_TAGS.has(tag)) {\n // SVG fragment without an <svg> wrapper — wrap, parse, unwrap.\n const wrapped = `<svg xmlns=\"${SVG_NS}\">${html}</svg>`;\n const doc = parseSvgOrThrow(wrapped, 'SVG fragment', html);\n const first = doc.documentElement.firstElementChild;\n /* c8 ignore next 2 — defensive: a successful XML parse of a wrapped svg always yields ≥1 child. */\n if (first === null) throw new Error(`toElement: SVG fragment produced no element\\n input: ${excerpt(html)}`);\n return first;\n }\n\n // HTML — `<template>`-based parse.\n const t = document.createElement('template');\n t.innerHTML = html;\n const child = t.content.firstElementChild;\n if (child === null) throw new Error(`toElement: produced no element\\n input: ${excerpt(html)}`);\n return child;\n}\n"]}
|
package/dist/jsx-runtime.d.ts
CHANGED
|
@@ -1,10 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Segment` — kerf's structured render output.
|
|
3
|
+
*
|
|
4
|
+
* The JSX runtime emits a `SafeHtml` wrapping a `Segment`. Most renders
|
|
5
|
+
* produce a single static segment (just an HTML string), which behaves
|
|
6
|
+
* exactly like a string for backward compatibility. When the tree
|
|
7
|
+
* contains a list (`each()`) or a parent whose children include a list,
|
|
8
|
+
* the runtime emits a structured segment that `mount()` can dispatch
|
|
9
|
+
* on — running its native keyed reconciler for the list parts and
|
|
10
|
+
* leaving the static surrounds to the general-purpose diff.
|
|
11
|
+
*
|
|
12
|
+
* Why have a structured form at all: the perf bottleneck for huge
|
|
13
|
+
* keyed lists isn't the per-row JSX work (which `each()` already
|
|
14
|
+
* memoises). It's that flattening every render's whole tree to one
|
|
15
|
+
* big HTML string forces a full `innerHTML` parse and a tree walk
|
|
16
|
+
* over rows we know are unchanged. The segment shape lets mount()
|
|
17
|
+
* skip both for the list parts.
|
|
18
|
+
*/
|
|
19
|
+
type Segment = StaticSegment | ListSegment | MixedSegment;
|
|
20
|
+
interface StaticSegment {
|
|
21
|
+
kind: 'static';
|
|
22
|
+
html: string;
|
|
23
|
+
}
|
|
24
|
+
interface ListItem {
|
|
25
|
+
/**
|
|
26
|
+
* The row's object identity. Used by the reconciler to match new items
|
|
27
|
+
* against live DOM nodes across renders. Unchanged ref → reuse the
|
|
28
|
+
* existing live node; replaced ref → build a fresh node.
|
|
29
|
+
*/
|
|
30
|
+
ref: object;
|
|
31
|
+
/**
|
|
32
|
+
* Optional cache-invalidation key that captures external state affecting
|
|
33
|
+
* this row's render (e.g. selection class). Different cacheKey on the
|
|
34
|
+
* same `ref` triggers a cache miss for that row. `undefined` when the
|
|
35
|
+
* user didn't pass a `key` callback to `each()`.
|
|
36
|
+
*/
|
|
37
|
+
cacheKey: unknown;
|
|
38
|
+
html: string;
|
|
39
|
+
}
|
|
40
|
+
interface ListSegment {
|
|
41
|
+
kind: 'list';
|
|
42
|
+
id: string;
|
|
43
|
+
items: ListItem[];
|
|
44
|
+
}
|
|
45
|
+
interface MixedSegment {
|
|
46
|
+
kind: 'mixed';
|
|
47
|
+
parts: Segment[];
|
|
48
|
+
}
|
|
49
|
+
|
|
1
50
|
/**
|
|
2
51
|
* kerf JSX runtime.
|
|
3
52
|
*
|
|
4
|
-
* JSX renders to `SafeHtml
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
53
|
+
* JSX renders to `SafeHtml`, which wraps both:
|
|
54
|
+
* - `__html`: the flattened HTML string (what `toString()` returns; what
|
|
55
|
+
* legacy/SSR consumers care about)
|
|
56
|
+
* - `__segment`: a structured representation that distinguishes "static
|
|
57
|
+
* html", "keyed list", and "mixed" content.
|
|
58
|
+
*
|
|
59
|
+
* Most renders are pure-static and the segment is just `{kind:'static',html}`.
|
|
60
|
+
* When the tree contains a list (via `each()`) or a parent whose children
|
|
61
|
+
* include a non-static segment, the runtime threads that structure up so
|
|
62
|
+
* `mount()` can dispatch on it — running its native keyed reconciler for
|
|
63
|
+
* the list parts and leaving the static surrounds to the general-purpose
|
|
64
|
+
* diff.
|
|
8
65
|
*
|
|
9
66
|
* Configure in your `tsconfig.json`:
|
|
10
67
|
*
|
|
@@ -14,11 +71,13 @@
|
|
|
14
71
|
* Then write JSX as you normally would — kerf provides the `jsx` /
|
|
15
72
|
* `jsxs` / `jsxDEV` / `Fragment` exports the JSX transform looks for.
|
|
16
73
|
*/
|
|
74
|
+
|
|
17
75
|
declare const SAFE_HTML_BRAND: unique symbol;
|
|
18
76
|
declare class SafeHtml {
|
|
19
77
|
readonly __html: string;
|
|
78
|
+
readonly __segment: Segment;
|
|
20
79
|
readonly [SAFE_HTML_BRAND]: true;
|
|
21
|
-
constructor(
|
|
80
|
+
constructor(input: string | Segment);
|
|
22
81
|
toString(): string;
|
|
23
82
|
}
|
|
24
83
|
/**
|
|
@@ -29,6 +88,11 @@ declare class SafeHtml {
|
|
|
29
88
|
declare function isSafeHtml(value: unknown): value is SafeHtml;
|
|
30
89
|
/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */
|
|
31
90
|
declare function raw(html: string): SafeHtml;
|
|
91
|
+
/**
|
|
92
|
+
* Internal: build a `SafeHtml` representing a list segment. Used by
|
|
93
|
+
* `each()` so the JSX runtime is the sole owner of `SafeHtml` construction.
|
|
94
|
+
*/
|
|
95
|
+
declare function listSafeHtml(id: string, items: ListSegment['items']): SafeHtml;
|
|
32
96
|
type Child = SafeHtml | string | number | boolean | null | undefined;
|
|
33
97
|
type Children = Child | Children[];
|
|
34
98
|
interface Props {
|
|
@@ -50,4 +114,4 @@ declare namespace JSX {
|
|
|
50
114
|
}
|
|
51
115
|
}
|
|
52
116
|
|
|
53
|
-
export { Fragment, JSX, SafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, raw };
|
|
117
|
+
export { Fragment, JSX, SafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw };
|
package/dist/jsx-runtime.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { Fragment, SafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, raw } from './chunk-
|
|
1
|
+
export { Fragment, SafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw } from './chunk-ZLV35OHG.js';
|
|
2
2
|
//# sourceMappingURL=jsx-runtime.js.map
|
|
3
3
|
//# sourceMappingURL=jsx-runtime.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kerfjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -64,7 +64,9 @@
|
|
|
64
64
|
"test:dist:full": "npm run build && vitest run --config vitest.config.dist-full.ts",
|
|
65
65
|
"lint": "eslint src tests",
|
|
66
66
|
"typecheck": "tsc --noEmit",
|
|
67
|
+
"check": "npm run lint && npm run typecheck && npm test && npm run build && vitest run --config vitest.config.dist.ts && vitest run --config vitest.config.dist-full.ts",
|
|
67
68
|
"clean": "rm -rf dist coverage node_modules/.cache",
|
|
69
|
+
"prepare": "husky",
|
|
68
70
|
"release": "bash scripts/release.sh",
|
|
69
71
|
"release:beta": "bash scripts/release.sh --beta",
|
|
70
72
|
"prepublishOnly": "npm run build",
|
|
@@ -72,8 +74,7 @@
|
|
|
72
74
|
"example:reactivity-demo:build": "cd examples/reactivity-demo && npm install && npm run build"
|
|
73
75
|
},
|
|
74
76
|
"dependencies": {
|
|
75
|
-
"@preact/signals-core": "^1.14.1"
|
|
76
|
-
"morphdom": "^2.7.8"
|
|
77
|
+
"@preact/signals-core": "^1.14.1"
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
|
79
80
|
"@types/jsdom": "^28.0.1",
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
"eslint": "^9.16.0",
|
|
85
86
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
86
87
|
"happy-dom": "^15.11.0",
|
|
88
|
+
"husky": "^9.1.7",
|
|
87
89
|
"jsdom": "^29.1.1",
|
|
88
90
|
"tsup": "^8.3.0",
|
|
89
91
|
"typescript": "^5.7.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/escapeHtml.ts","../src/utils/jsx-attr-aliases.ts","../src/jsx-runtime.ts"],"names":[],"mappings":"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","file":"chunk-URMYMSGU.js","sourcesContent":["/**\n * HTML / attribute escaping for the JSX runtime. Identical to the helpers\n * used in any reasonable HTML emitter — included here so kerf has no extra\n * runtime dependencies beyond `@preact/signals-core` and `morphdom`.\n */\n\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n}\n","/**\n * JSX → HTML / SVG attribute name aliases.\n *\n * The JSX runtime translates camelCase attributes (React convention) to\n * the kebab-case / colon-form names the browser actually wants. Anything\n * not in this map is passed through verbatim — `data-*`, `aria-*`, and\n * any custom attribute work without ceremony.\n *\n * Lives in its own module so `src/jsx-runtime.ts` can stay under the\n * 200-LOC project guideline; the bulk of `jsx-runtime.ts` was this table.\n */\n\nexport const ATTR_ALIASES: Record<string, string> = {\n // HTML attributes\n className: 'class',\n htmlFor: 'for',\n httpEquiv: 'http-equiv',\n acceptCharset: 'accept-charset',\n accessKey: 'accesskey',\n autoCapitalize: 'autocapitalize',\n autoComplete: 'autocomplete',\n autoFocus: 'autofocus',\n autoPlay: 'autoplay',\n colSpan: 'colspan',\n contentEditable: 'contenteditable',\n crossOrigin: 'crossorigin',\n dateTime: 'datetime',\n defaultChecked: 'checked',\n defaultValue: 'value',\n encType: 'enctype',\n formAction: 'formaction',\n formEncType: 'formenctype',\n formMethod: 'formmethod',\n formNoValidate: 'formnovalidate',\n formTarget: 'formtarget',\n hrefLang: 'hreflang',\n inputMode: 'inputmode',\n maxLength: 'maxlength',\n minLength: 'minlength',\n noModule: 'nomodule',\n noValidate: 'novalidate',\n readOnly: 'readonly',\n referrerPolicy: 'referrerpolicy',\n rowSpan: 'rowspan',\n spellCheck: 'spellcheck',\n srcDoc: 'srcdoc',\n srcLang: 'srclang',\n srcSet: 'srcset',\n tabIndex: 'tabindex',\n useMap: 'usemap',\n\n // SVG presentation attributes (camelCase → kebab-case)\n strokeWidth: 'stroke-width',\n strokeLinecap: 'stroke-linecap',\n strokeLinejoin: 'stroke-linejoin',\n strokeDasharray: 'stroke-dasharray',\n strokeDashoffset: 'stroke-dashoffset',\n strokeMiterlimit: 'stroke-miterlimit',\n strokeOpacity: 'stroke-opacity',\n fillOpacity: 'fill-opacity',\n fillRule: 'fill-rule',\n clipPath: 'clip-path',\n clipRule: 'clip-rule',\n colorInterpolation: 'color-interpolation',\n colorInterpolationFilters: 'color-interpolation-filters',\n floodColor: 'flood-color',\n floodOpacity: 'flood-opacity',\n lightingColor: 'lighting-color',\n stopColor: 'stop-color',\n stopOpacity: 'stop-opacity',\n shapeRendering: 'shape-rendering',\n imageRendering: 'image-rendering',\n textRendering: 'text-rendering',\n pointerEvents: 'pointer-events',\n vectorEffect: 'vector-effect',\n paintOrder: 'paint-order',\n\n // SVG text/font attributes\n fontFamily: 'font-family',\n fontSize: 'font-size',\n fontStyle: 'font-style',\n fontVariant: 'font-variant',\n fontWeight: 'font-weight',\n fontStretch: 'font-stretch',\n textAnchor: 'text-anchor',\n textDecoration: 'text-decoration',\n dominantBaseline: 'dominant-baseline',\n alignmentBaseline: 'alignment-baseline',\n baselineShift: 'baseline-shift',\n letterSpacing: 'letter-spacing',\n wordSpacing: 'word-spacing',\n writingMode: 'writing-mode',\n\n // SVG marker attributes\n markerStart: 'marker-start',\n markerMid: 'marker-mid',\n markerEnd: 'marker-end',\n\n // SVG xlink (legacy but still used)\n xlinkHref: 'xlink:href',\n xlinkShow: 'xlink:show',\n xlinkActuate: 'xlink:actuate',\n xlinkType: 'xlink:type',\n xlinkRole: 'xlink:role',\n xlinkTitle: 'xlink:title',\n xlinkArcrole: 'xlink:arcrole',\n xmlBase: 'xml:base',\n xmlLang: 'xml:lang',\n xmlSpace: 'xml:space',\n xmlnsXlink: 'xmlns:xlink',\n};\n","/**\n * kerf JSX runtime.\n *\n * JSX renders to `SafeHtml` — a wrapped HTML string. `SafeHtml.toString()`\n * is what the consumer eventually feeds into `mount()` (which morphs the\n * live DOM toward the new tree) or into `toElement()` (which parses it\n * to a single DOM node).\n *\n * Configure in your `tsconfig.json`:\n *\n * \"jsx\": \"react-jsx\",\n * \"jsxImportSource\": \"kerfjs\"\n *\n * Then write JSX as you normally would — kerf provides the `jsx` /\n * `jsxs` / `jsxDEV` / `Fragment` exports the JSX transform looks for.\n */\n\nimport { escapeAttr, escapeHtml } from './utils/escapeHtml.js';\nimport { ATTR_ALIASES } from './utils/jsx-attr-aliases.js';\n\n// Cross-realm/cross-bundle brand. Using `Symbol.for` (the global registry)\n// means two `SafeHtml` classes from different module copies still recognise\n// each other. Same approach React uses for `$$typeof: Symbol.for('react.element')`.\n// Without this, `instanceof SafeHtml` fails when the consumer's bundler ends\n// up loading two copies of kerf (separate barrel + jsx-runtime entries,\n// monorepo dedup misses, ESM/CJS interop, etc.).\nconst SAFE_HTML_BRAND = Symbol.for('kerfjs.SafeHtml');\n\nexport class SafeHtml {\n readonly __html: string;\n // Branded so `isSafeHtml()` recognises instances from any copy of this module.\n readonly [SAFE_HTML_BRAND] = true as const;\n constructor(html: string) {\n this.__html = html;\n }\n toString(): string {\n return this.__html;\n }\n}\n\n/**\n * Type guard for `SafeHtml`. Prefer this over `instanceof SafeHtml` — it works\n * across module copies (e.g. when the consumer's bundler loads kerf's barrel\n * and JSX-runtime entries as independent modules).\n */\nexport function isSafeHtml(value: unknown): value is SafeHtml {\n return typeof value === 'object'\n && value !== null\n && (value as Record<symbol, unknown>)[SAFE_HTML_BRAND] === true;\n}\n\n/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */\nexport function raw(html: string): SafeHtml {\n return new SafeHtml(html);\n}\n\ntype Child = SafeHtml | string | number | boolean | null | undefined;\ntype Children = Child | Children[];\n\ninterface Props {\n children?: Children;\n [key: string]: unknown;\n}\n\nconst VOID_TAGS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'source', 'track', 'wbr',\n]);\n\nfunction renderChildren(children: Children): string {\n if (children == null || typeof children === 'boolean') return '';\n if (isSafeHtml(children)) return children.__html;\n if (typeof children === 'string') return escapeHtml(children);\n if (typeof children === 'number') return String(children);\n if (Array.isArray(children)) return children.map(renderChildren).join('');\n // Catch the common mistake of passing a DOM element (e.g. the result of\n // toElement(...)) as a JSX child. The runtime renders to HTML strings, so\n // DOM nodes can't be composed — they'd silently serialize to \"\" and their\n // event listeners would be lost. Throw loudly so this can't sneak in.\n const maybeNode = children as unknown;\n if (typeof maybeNode === 'object' && maybeNode !== null\n && ('nodeType' in maybeNode || 'outerHTML' in maybeNode)) {\n throw new Error(\n 'JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). '\n + 'Build the tree in one JSX expression and use querySelector after toElement() to get element refs.',\n );\n }\n throw new Error(\n `JSX: unsupported child of type ${describeValue(children)}. `\n + 'Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. '\n + 'Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), '\n + 'passing a function (call it first), or passing a Promise (await it before render).',\n );\n}\n\nfunction describeValue(v: unknown): string {\n if (Array.isArray(v)) return 'array';\n if (typeof v === 'object' && v !== null) {\n const ctor = (v as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== 'Object' ? `object (${ctor})` : 'object';\n }\n return typeof v;\n}\n\nfunction renderAttr(key: string, value: unknown): string {\n const name = ATTR_ALIASES[key] ?? key;\n if (value == null || value === false) return '';\n if (value === true) return ` ${name}`;\n let strValue: string;\n if (isSafeHtml(value)) {\n strValue = value.__html;\n } else if (typeof value === 'number') {\n strValue = String(value);\n } else if (typeof value === 'string') {\n strValue = escapeAttr(value);\n } else {\n throw new Error(\n `JSX: unsupported value for attribute \"${key}\" — got ${describeValue(value)}. `\n + 'Attribute values must be string, number, boolean, null, undefined, or SafeHtml. '\n + 'Did you mean to read .value off a Signal, or stringify the object first?',\n );\n }\n return ` ${name}=\"${strValue}\"`;\n}\n\nexport function jsx(tag: string | ((props: Props) => SafeHtml), props: Props): SafeHtml {\n if (typeof tag === 'function') return tag(props);\n\n const { children, ...attrs } = props;\n const attrStr = Object.entries(attrs)\n .map(([k, v]) => renderAttr(k, v))\n .join('');\n\n if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);\n\n const childStr = children != null ? renderChildren(children) : '';\n return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);\n}\n\nexport { jsx as jsxs };\n// vitest's dev-mode JSX transform emits `jsxDEV(tag, props, ...)`; the\n// alias lets tests import this module without the production build pipeline\n// caring.\nexport { jsx as jsxDEV };\n\nexport function Fragment({ children }: { children?: Children }): SafeHtml {\n return new SafeHtml(children != null ? renderChildren(children) : '');\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace JSX {\n export type Element = SafeHtml;\n export interface ElementChildrenAttribute {\n children: unknown;\n }\n export interface IntrinsicElements {\n [elemName: string]: Record<string, unknown>;\n }\n}\n"]}
|