Sign In

kerfjs

Package Overview
Dependencies
Maintainers
1
Versions
49
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

kerfjs - npm Package Compare versions

Comparing version
4.2.0-beta.4
to
4.2.0-beta.5
+45
dist/attach.d.ts
/**
* `kerfjs/attach` — bind a non-kerf widget's lifecycle to a single DOM node.
*
* `data-morph-skip` lets a library own a subtree so kerf won't touch it — but
* nothing manages that widget's LIFECYCLE. You set it up imperatively after
* render and must remember to tear it down when the node is replaced/removed
* (dropping document-level listeners the widget added, etc.). `attach` closes
* that seam: run a setup against one **existing** DOM node and auto-run its
* teardown when that node leaves the document.
*
* import { attach } from 'kerfjs/attach';
*
* attach(canvasEl, (el) => {
* const chart = D3.mount(el);
* return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)
* });
*
* `setup(node)` runs immediately — the node already exists, so there is nothing
* to wait for (this is NOT React's `useEffect`: no dependency array, no re-run,
* no render-phase or hook-order scoping; it is closer to a Web Component's
* `connectedCallback`/`disconnectedCallback` pair, Svelte's
* `onMount(() => () => cleanup)`, or Solid's `onCleanup`). The returned teardown
* runs once — whichever comes first — when the node leaves the document (detected
* by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any
* removal triggers it) or when the returned disposer is called. Re-creation is
* NOT handled here: a fresh node is a fresh `attach()` call — pair it with
* `kerfjs/remount`, which replaces the node and re-runs your render (and thus
* this call) on the new one.
*
* Related: `kerfjs/scope`'s `observeRemovals` also auto-disposes on removal via a
* `MutationObserver`, but scoped to a whole subtree's registered disposers rather
* than one node's setup/teardown pair — reach for that when you're collecting
* many disposers under an element, and for `attach` when you're binding one
* widget's lifecycle to one node.
*/
/** The setup callback for {@link attach}: run against `node`, optionally return a teardown. */
type AttachSetup = (node: Element) => (() => void) | void;
/**
* Run `setup(node)` now, and its returned teardown once — when `node` leaves the
* document, or when the returned disposer is called, whichever is first. Returns
* a disposer (idempotent) so a `mount()` / `Scope` can drive teardown explicitly.
*/
declare function attach(node: Element, setup: AttachSetup): () => void;
export { type AttachSetup, attach };
// src/attach.ts
function attach(node, setup) {
const teardown = setup(node);
let done = false;
const finish = () => {
if (done) return;
done = true;
observer.disconnect();
if (typeof teardown === "function") teardown();
};
const observer = new MutationObserver(() => {
if (!node.isConnected) finish();
});
observer.observe(node.getRootNode(), { childList: true, subtree: true });
return finish;
}
export { attach };
//# sourceMappingURL=attach.js.map
//# sourceMappingURL=attach.js.map
{"version":3,"sources":["../src/attach.ts"],"names":[],"mappings":";AA4CO,SAAS,MAAA,CAAO,MAAe,KAAA,EAAgC;AACpE,EAAA,MAAM,QAAA,GAAW,MAAM,IAAI,CAAA;AAC3B,EAAA,IAAI,IAAA,GAAO,KAAA;AAEX,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,IAAI,IAAA,EAAM;AACV,IAAA,IAAA,GAAO,IAAA;AACP,IAAA,QAAA,CAAS,UAAA,EAAW;AACpB,IAAA,IAAI,OAAO,QAAA,KAAa,UAAA,EAAY,QAAA,EAAS;AAAA,EAC/C,CAAA;AAMA,EAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM;AAC1C,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAa,MAAA,EAAO;AAAA,EAChC,CAAC,CAAA;AACD,EAAA,QAAA,CAAS,OAAA,CAAQ,KAAK,WAAA,EAAY,EAAG,EAAE,SAAA,EAAW,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAEvE,EAAA,OAAO,MAAA;AACT","file":"attach.js","sourcesContent":["/**\n * `kerfjs/attach` — bind a non-kerf widget's lifecycle to a single DOM node.\n *\n * `data-morph-skip` lets a library own a subtree so kerf won't touch it — but\n * nothing manages that widget's LIFECYCLE. You set it up imperatively after\n * render and must remember to tear it down when the node is replaced/removed\n * (dropping document-level listeners the widget added, etc.). `attach` closes\n * that seam: run a setup against one **existing** DOM node and auto-run its\n * teardown when that node leaves the document.\n *\n * import { attach } from 'kerfjs/attach';\n *\n * attach(canvasEl, (el) => {\n * const chart = D3.mount(el);\n * return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)\n * });\n *\n * `setup(node)` runs immediately — the node already exists, so there is nothing\n * to wait for (this is NOT React's `useEffect`: no dependency array, no re-run,\n * no render-phase or hook-order scoping; it is closer to a Web Component's\n * `connectedCallback`/`disconnectedCallback` pair, Svelte's\n * `onMount(() => () => cleanup)`, or Solid's `onCleanup`). The returned teardown\n * runs once — whichever comes first — when the node leaves the document (detected\n * by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any\n * removal triggers it) or when the returned disposer is called. Re-creation is\n * NOT handled here: a fresh node is a fresh `attach()` call — pair it with\n * `kerfjs/remount`, which replaces the node and re-runs your render (and thus\n * this call) on the new one.\n *\n * Related: `kerfjs/scope`'s `observeRemovals` also auto-disposes on removal via a\n * `MutationObserver`, but scoped to a whole subtree's registered disposers rather\n * than one node's setup/teardown pair — reach for that when you're collecting\n * many disposers under an element, and for `attach` when you're binding one\n * widget's lifecycle to one node.\n */\n\n/** The setup callback for {@link attach}: run against `node`, optionally return a teardown. */\nexport type AttachSetup = (node: Element) => (() => void) | void;\n\n/**\n * Run `setup(node)` now, and its returned teardown once — when `node` leaves the\n * document, or when the returned disposer is called, whichever is first. Returns\n * a disposer (idempotent) so a `mount()` / `Scope` can drive teardown explicitly.\n */\nexport function attach(node: Element, setup: AttachSetup): () => void {\n const teardown = setup(node);\n let done = false;\n\n const finish = (): void => {\n if (done) return;\n done = true;\n observer.disconnect();\n if (typeof teardown === 'function') teardown();\n };\n\n // Observe the node's live tree (the document when connected) with subtree, so\n // an ANCESTOR removal — not just a direct one — is caught. Each mutation just\n // re-checks `node.isConnected`, which is true until the node (or an ancestor)\n // is removed, so a morph swap / remountOn replacement / manual removal all fire.\n const observer = new MutationObserver(() => {\n if (!node.isConnected) finish();\n });\n observer.observe(node.getRootNode(), { childList: true, subtree: true });\n\n return finish;\n}\n"]}
import { itemVersion } from './chunk-QIP723L4.js';
import { parseRowTemplate, rowContractError, parseSingleRow, collectTemplateChildren } from './chunk-YHH7OUFA.js';
import { captureRowBindings, listSafeHtml, boundTextNodeOf, syncFormProp, newBindingContext, wireBindings, disposeRowBindings, isSafeHtml, wireRowBindings, _setBindingContext, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX, carryOrRewireRowBindings, granularListSafeHtml } from './chunk-SUPUPSBE.js';
import { effect } from './chunk-3APBEVHF.js';
import { LIST_MARKER_PREFIX, flattenWithoutListItems, collectLists, flatten } from './chunk-GY4XV2UV.js';
import { devHooks } from './chunk-VVDJLWMP.js';
// src/list-render-state.ts
function deriveListRenderState(bindingCount) {
if (bindingCount === void 0) return "unbound";
return bindingCount === 0 ? "empty" : "bound";
}
function decideListPath(state, patches, snapshotLength, previousBindingCount) {
if (state === "unbound") return { path: "snapshot", reason: "first-render" };
if (state === "empty") return { path: "snapshot", reason: "empty-binding" };
if (patches.length === 0) return { path: "snapshot", reason: "no-patches" };
let netDelta = 0;
for (const p of patches) {
if (p.type === "insert") netDelta += 1;
else if (p.type === "remove") netDelta -= 1;
else if (p.type === "replace") return { path: "snapshot", reason: "replace" };
}
const count = previousBindingCount ?? 0;
if (count + netDelta !== snapshotLength) {
return { path: "snapshot", reason: "count-drift" };
}
return { path: "granular" };
}
// src/each.ts
var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
function isArraySignal(value) {
return typeof value === "object" && value !== null && value[ARRAY_SIGNAL_BRAND] === true;
}
var context = null;
var renderingRow = false;
function inRowScope(fn) {
const prev = renderingRow;
renderingRow = true;
try {
return fn();
} finally {
renderingRow = prev;
}
}
function _setRenderContext(c) {
context = c;
}
function _resetCallOrderListState(ctx) {
const isCallOrderId = (id) => !id.startsWith("k:");
for (const map of [ctx.caches, ctx.bindingCounts, ctx.bindingSources]) {
for (const id of Array.from(map.keys())) {
if (isCallOrderId(id)) map.delete(id);
}
}
}
function isEachOptions(v) {
return typeof v === "object" && v !== null;
}
var VALID_KEY = /^[A-Za-z0-9_.:/-]+$/;
function assertValidKey(key) {
if (typeof key !== "string" || !VALID_KEY.test(key) || key.includes("--")) {
throw new Error(
`each(): invalid list key ${JSON.stringify(key)}. A key must be a non-empty string of letters, digits, or _ . : / - (and may not contain "--"), because kerf writes it into the list's marker comment in the DOM. Use a short stable identifier, e.g. { key: 'results' }.`
);
}
}
function claimKey(ctx, key) {
assertValidKey(key);
if (renderingRow) {
throw new Error(
`each(): list key ${JSON.stringify(key)} was used by an each() inside a row render. A nested each() is not reconciled \u2014 the row is flattened to HTML, so the inner list never binds and would render as static markup. Render the inner collection with plain .map() (it re-renders with its row), or restructure to a flat list.`
);
}
if (ctx.keysThisRender.has(key)) {
throw new Error(
`each(): duplicate list key ${JSON.stringify(key)}. Every keyed each() in a mount must have its own key \u2014 two lists sharing one would share the same cache, binding and DOM anchor. Give each list a distinct key.`
);
}
ctx.keysThisRender.add(key);
return `k:${key}`;
}
function each(items, render, cacheKeyOrOptions) {
const useOptions = isEachOptions(cacheKeyOrOptions);
const cacheKey = useOptions ? cacheKeyOrOptions.cacheKey : cacheKeyOrOptions;
const listKey = useOptions ? cacheKeyOrOptions.key : void 0;
if (isArraySignal(items) && context !== null) {
return eachGranular(items, render, cacheKey, listKey);
}
const snapshotItems = isArraySignal(items) ? items.value : items;
return eachSnapshot(snapshotItems, render, cacheKey, listKey);
}
function eachSnapshot(items, render, cacheKey, listKey) {
let id;
if (context !== null) {
id = listKey !== void 0 ? claimKey(context, listKey) : String(context.counter++);
} else {
id = "orphan";
}
return eachSnapshotById(items, render, cacheKey, id);
}
function assertObjectItem(item, index) {
if (typeof item !== "object" || item === null) {
throw new Error(
`each(): items must be objects (the per-item HTML cache is a WeakMap), got ${item === null ? "null" : typeof item} at index ${index}. Wrap primitives if you need to iterate them, e.g. items.map(v => ({ v })).`
);
}
}
function eachGranular(sig, render, cacheKey, listKey) {
const ctx = context;
const id = listKey !== void 0 ? claimKey(ctx, listKey) : String(ctx.counter++);
const previousBindingCount = ctx.bindingCounts.get(id);
const patches = sig._consumePatches();
const snapshot = sig.value;
const previousSource = ctx.bindingSources.get(id);
const sourceReused = ctx.bindingSources.has(id) && previousSource !== sig;
if (sourceReused && listKey === void 0) ctx.shiftCandidates.push(id);
const decision = sourceReused ? { path: "snapshot" } : decideListPath(
deriveListRenderState(previousBindingCount),
patches,
snapshot.length,
previousBindingCount
);
if (decision.path === "snapshot") {
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
let staleIndexShift = false;
if (render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
const rendered = [];
for (let i = 0; i < previousBindingCount; i++) rendered.push(i);
for (const p of patches) {
if (p.type === "insert") rendered.splice(p.index, 0, p.index);
else if (p.type === "remove") rendered.splice(p.index, 1);
else if (p.type === "move") {
const [moved] = rendered.splice(p.from, 1);
rendered.splice(p.to, 0, moved);
}
}
for (let i = 0; i < rendered.length; i++) {
if (rendered[i] !== i) {
staleIndexShift = true;
break;
}
}
}
if (cacheKey !== void 0) {
const cache2 = ctx.caches.get(id);
for (let i = 0; i < snapshot.length; i++) {
const item = snapshot[i];
const k = cacheKey(item, i);
const cached = cache2.get(item);
if (cached !== void 0 && cached.cacheKey !== k) {
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
}
}
const renderRow = (item, index) => captureRowBindings(() => inRowScope(() => {
const out = render(item, index);
return isSafeHtml(out) ? out.toString() : out;
}));
const internalPatches = new Array(patches.length);
const cache = ctx.caches.get(id);
try {
for (let i = 0; i < patches.length; i++) {
const p = patches[i];
if (p.type === "insert" || p.type === "update") {
assertObjectItem(p.item, p.index);
const { html, bindings } = renderRow(p.item, p.index);
internalPatches[i] = {
type: p.type,
index: p.index,
item: p.item,
html,
bindings
};
cache?.set(p.item, {
cacheKey: cacheKey ? cacheKey(p.item, p.index) : void 0,
html,
bindings,
version: itemVersion(p.item),
index: p.index
});
} else {
internalPatches[i] = p;
}
}
} catch {
ctx.bindingCounts.delete(id);
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
if (staleIndexShift) devHooks.staleIndex?.(id);
return granularListSafeHtml(id, [], internalPatches, sig);
}
function eachSnapshotById(items, render, cacheKey, id, source) {
let cache = null;
if (context !== null) {
let c = context.caches.get(id);
if (c === void 0) {
c = /* @__PURE__ */ new WeakMap();
context.caches.set(id, c);
}
cache = c;
}
const segItems = new Array(items.length);
const seen = /* @__PURE__ */ new Set();
for (let i = 0; i < items.length; i++) {
const item = items[i];
assertObjectItem(item, i);
if (seen.has(item)) {
throw new Error(
`each(): the same object reference appears at multiple indices in items (first seen earlier, again at index ${i}). The per-item HTML cache is keyed on object identity, so duplicate references break the keyed reconciler and can leak DOM nodes on re-render. Use a fresh object per row (e.g. items.map(o => ({ ...o })) before passing to each()).`
);
}
seen.add(item);
const k = cacheKey ? cacheKey(item, i) : void 0;
const version = itemVersion(item);
let html;
let bindings;
const cached = cache !== null ? cache.get(item) : void 0;
if (cached !== void 0 && cached.cacheKey === k && cached.version === version) {
html = cached.html;
bindings = cached.bindings;
if (cached.index !== i && render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
devHooks.staleIndex?.(id);
}
} else {
const captured = captureRowBindings(() => inRowScope(() => {
const out = render(item, i);
return isSafeHtml(out) ? out.toString() : out;
}));
html = captured.html;
bindings = captured.bindings;
if (cache !== null) cache.set(item, { cacheKey: k, html, bindings, version, index: i });
}
segItems[i] = { ref: item, cacheKey: k, html, bindings };
}
if (cacheKey !== void 0) {
devHooks.duplicateCacheKeys?.(id, segItems);
}
return listSafeHtml(id, segItems, source);
}
// src/list-reconcile-focus.ts
function captureFocus(liveParent) {
const active = document.activeElement;
if (active === null || active === document.body) return null;
if (!liveParent.contains(active)) return null;
const el = active;
let selStart = null;
let selEnd = null;
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
try {
selStart = el.selectionStart;
selEnd = el.selectionEnd;
} catch {
}
}
return { el, selStart, selEnd };
}
function restoreFocus(snap) {
if (document.activeElement === snap.el) return;
if (!snap.el.isConnected) return;
snap.el.focus();
if (snap.selStart !== null && snap.selEnd !== null) {
try {
snap.el.setSelectionRange(snap.selStart, snap.selEnd);
} catch {
}
}
}
// src/morph.ts
var ID_KEY_PREFIX = "id:";
var DATA_KEY_PREFIX = "data-key:";
var ELEMENT_NODE = 1;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;
function getNodeKey(node) {
if (node.nodeType !== ELEMENT_NODE) return void 0;
const el = node;
if (el.id !== "") return `${ID_KEY_PREFIX}${el.id}`;
if (el.dataset !== void 0 && el.dataset.key !== void 0) {
return `${DATA_KEY_PREFIX}${el.dataset.key}`;
}
return void 0;
}
var EMPTY_OWNED = /* @__PURE__ */ new Set();
function morph(liveRoot, template, ownedItems = EMPTY_OWNED) {
if (liveRoot == null) {
throw new Error(
'morph: liveRoot is null/undefined \u2014 pass the live element, e.g. morph(document.getElementById("app")!, template). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say Element.'
);
}
const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template);
const focusSnap = captureFocus(liveRoot);
morphChildren(liveRoot, templateEl, ownedItems);
if (focusSnap !== null) restoreFocus(focusSnap);
}
function _morphElement(fromEl, toEl, ownedItems = EMPTY_OWNED) {
morphElement(fromEl, toEl, ownedItems);
}
function isElementNode(t) {
return typeof t === "object" && t !== null && t.nodeType === ELEMENT_NODE;
}
function parseTemplate(liveRoot, template) {
const el = liveRoot.cloneNode(false);
el.innerHTML = String(template);
return el;
}
function protectionTag(node) {
const { dataset } = node;
return (dataset.morphSkip !== void 0 ? "s" : "") + (dataset.morphSkipChildren !== void 0 ? "c" : "") + (dataset.morphPreserve !== void 0 ? "p" : "");
}
var MARKER_PREFIXES = [LIST_MARKER_PREFIX, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX];
function isMarker(node) {
if (node.nodeType !== COMMENT_NODE) return false;
const { data } = node;
return MARKER_PREFIXES.some((prefix) => data.startsWith(prefix));
}
function markersPairable(a, b) {
if (!isMarker(a) && !isMarker(b)) return true;
return a.data === b.data;
}
function skipOwned(node, ownedItems) {
while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) {
node = node.nextSibling;
}
return node;
}
function isListMarker(node) {
return node.nodeType === COMMENT_NODE && node.data.startsWith(LIST_MARKER_PREFIX);
}
function afterListRegion(marker, ownedItems) {
let last = marker;
for (let r = marker.nextSibling; r !== null; r = r.nextSibling) {
if (isListMarker(r)) break;
if (r.nodeType === ELEMENT_NODE && ownedItems.has(r)) last = r;
}
return last.nextSibling;
}
function morphChildren(fromParent, toParent, ownedItems) {
const keyed = /* @__PURE__ */ new Map();
for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue;
const k = getNodeKey(c);
if (k !== void 0) keyed.set(k, c);
}
let fromChild = skipOwned(fromParent.firstChild, ownedItems);
let toChild = toParent.firstChild;
while (toChild !== null) {
const toNext = toChild.nextSibling;
let matched = null;
const toKey = getNodeKey(toChild);
if (toKey !== void 0 && keyed.has(toKey)) {
matched = keyed.get(toKey);
keyed.delete(toKey);
if (matched !== fromChild) {
fromParent.insertBefore(matched, fromChild);
} else {
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
}
}
if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && markersPairable(fromChild, toChild) && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0 && toKey === void 0 && protectionTag(fromChild) === protectionTag(toChild))) {
matched = fromChild;
fromChild = skipOwned(
isListMarker(matched) ? afterListRegion(matched, ownedItems) : fromChild.nextSibling,
ownedItems
);
if (matched.nodeType === COMMENT_NODE && fromChild !== null) {
const owned = boundTextNodeOf(matched);
if (owned !== null && fromChild === owned) {
fromChild = skipOwned(owned.nextSibling, ownedItems);
}
}
}
if (matched === null && toChild.nodeType === ELEMENT_NODE && fromChild !== null && toKey === void 0) {
const toTag = toChild.tagName;
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
if (scan.nodeType !== ELEMENT_NODE) continue;
const el = scan;
if (ownedItems.has(el)) continue;
if (el.tagName !== toTag || getNodeKey(el) !== void 0) continue;
if (protectionTag(el) !== protectionTag(toChild)) continue;
matched = el;
fromParent.insertBefore(el, fromChild);
break;
}
}
if (matched === null && fromChild !== null && toChild.nodeType === COMMENT_NODE && toChild.data.startsWith(LIST_MARKER_PREFIX)) {
const wantData = toChild.data;
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
if (scan.nodeType !== COMMENT_NODE || scan.data !== wantData) continue;
const regionEnd = afterListRegion(scan, ownedItems);
const run = [];
for (let r = scan; r !== null && r !== regionEnd; r = r.nextSibling) {
run.push(r);
}
const focusSnap = captureFocus(fromParent);
for (const node of run) fromParent.insertBefore(node, fromChild);
if (focusSnap !== null) restoreFocus(focusSnap);
matched = scan;
break;
}
}
if (matched !== null) {
morphNode(matched, toChild, ownedItems);
} else {
const cloned = toChild.cloneNode(true);
fromParent.insertBefore(cloned, fromChild);
}
toChild = toNext;
}
while (fromChild !== null) {
const next = fromChild.nextSibling;
if (fromChild.nodeType === ELEMENT_NODE) {
const el = fromChild;
if (!ownedItems.has(el) && el.dataset.morphPreserve === void 0) {
fromParent.removeChild(fromChild);
}
} else {
fromParent.removeChild(fromChild);
}
fromChild = next;
}
}
function morphNode(fromNode, toNode, ownedItems) {
if (fromNode.nodeType === ELEMENT_NODE) {
morphElement(fromNode, toNode, ownedItems);
return;
}
if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) {
const fromText = fromNode;
const toText = toNode;
if (fromText.data !== toText.data) fromText.data = toText.data;
}
}
function morphElement(fromEl, toEl, ownedItems) {
if (fromEl.tagName !== toEl.tagName) {
const replacement = toEl.cloneNode(true);
fromEl.parentNode?.replaceChild(replacement, fromEl);
return;
}
if (fromEl.dataset.morphSkip !== void 0) return;
if (fromEl.isEqualNode(toEl)) return;
if (fromEl === document.activeElement) {
const ce = fromEl.getAttribute("contenteditable");
if (ce !== null && ce.toLowerCase() !== "false") return;
if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
}
morphAttributes(fromEl, toEl);
if (fromEl.dataset.morphSkipChildren !== void 0) return;
const syncTextareaValue = fromEl.tagName === "TEXTAREA" && fromEl !== document.activeElement && fromEl.textContent !== toEl.textContent;
morphChildren(fromEl, toEl, ownedItems);
if (syncTextareaValue) {
fromEl.value = toEl.textContent;
}
}
function isUserAgentOwnedAttr(tagName, name) {
return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG");
}
function morphAttributes(fromEl, toEl) {
const toAttrs = toEl.attributes;
for (let i = 0; i < toAttrs.length; i++) {
const attr = toAttrs[i];
const ns = attr.namespaceURI;
const name = attr.localName;
const value = attr.value;
if (ns !== null) {
if (fromEl.getAttributeNS(ns, name) !== value) {
fromEl.setAttributeNS(ns, attr.name, value);
}
} else if (fromEl.getAttribute(name) !== value) {
fromEl.setAttribute(name, value);
syncFormProp(fromEl, name, value, true);
}
}
const fromAttrs = fromEl.attributes;
const fromTag = fromEl.tagName;
for (let i = fromAttrs.length - 1; i >= 0; i--) {
const attr = fromAttrs[i];
const ns = attr.namespaceURI;
const name = attr.localName;
if (ns !== null) {
if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);
} else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) {
fromEl.removeAttribute(name);
syncFormProp(fromEl, name, "", false);
}
}
}
function isTextInputOrTextarea(el) {
if (el.tagName === "TEXTAREA") return true;
if (el.tagName === "INPUT") {
const type = el.type;
return type === "text" || type === "search" || type === "url" || type === "email" || type === "tel" || type === "password" || type === "";
}
return false;
}
function preserveTextEntryState(fromEl, toEl) {
if (fromEl.tagName === "TEXTAREA" || fromEl.tagName === "INPUT") {
const fromInput = fromEl;
const toInput = toEl;
toInput.value = fromInput.value;
try {
toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd);
} catch {
}
}
}
// src/list-binding.ts
function endAnchor(binding) {
if (binding.items.length > 0) {
return binding.items[binding.items.length - 1].node.nextSibling;
}
return binding.marker.nextSibling;
}
// src/list-reconcile-fast-paths.ts
var LT = 60;
var GT = 62;
var DQUOTE = 34;
var SQUOTE = 39;
var AMP = 38;
var EQ = 61;
var SLASH = 47;
var TEXT_NODE2 = 3;
var ELEMENT_NODE2 = 1;
function isWhitespace(cc) {
return cc === 32 || cc === 9 || cc === 10 || cc === 13;
}
function tryAttributeOnlyFastPath(liveNode, oldHtml, newHtml) {
const oldGt = oldHtml.indexOf(">");
const newGt = newHtml.indexOf(">");
if (oldGt === -1 || newGt === -1) return false;
if (oldHtml.length - oldGt !== newHtml.length - newGt) return false;
if (oldHtml.slice(oldGt) !== newHtml.slice(newGt)) return false;
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
const oldTag = parseOpeningTag(oldHtml, oldGt);
const newTag = parseOpeningTag(newHtml, newGt);
if (oldTag === null || newTag === null) return false;
if (oldTag.tagName !== newTag.tagName) return false;
for (const name of oldTag.attrs.keys()) {
if (name.indexOf(":") !== -1) return false;
}
for (const name of newTag.attrs.keys()) {
if (name.indexOf(":") !== -1) return false;
}
const liveTagUpper = liveNode.tagName;
for (const [name, rawValue] of newTag.attrs) {
const oldValue = oldTag.attrs.get(name);
if (oldValue === rawValue) continue;
const value = unescapeAttrValue(rawValue);
liveNode.setAttribute(name, value);
syncFormProp(liveNode, name, value, true);
}
for (const name of oldTag.attrs.keys()) {
if (newTag.attrs.has(name)) continue;
if (isUserAgentOwnedAttr2(liveTagUpper, name)) continue;
liveNode.removeAttribute(name);
syncFormProp(liveNode, name, "", false);
}
return true;
}
function tryTextContentFastPath(liveNode, oldHtml, newHtml) {
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
let p = 0;
const minLen = Math.min(oldHtml.length, newHtml.length);
while (p < minLen && oldHtml.charCodeAt(p) === newHtml.charCodeAt(p)) p++;
let s = 0;
const maxS = minLen - p;
while (s < maxS && oldHtml.charCodeAt(oldHtml.length - 1 - s) === newHtml.charCodeAt(newHtml.length - 1 - s)) {
s++;
}
const oldWinEnd = oldHtml.length - s;
const newWinEnd = newHtml.length - s;
if (!isPureTextWindow(oldHtml, p, oldWinEnd)) return false;
if (!isPureTextWindow(newHtml, p, newWinEnd)) return false;
if (p === 0) return false;
const boundaryCc = oldHtml.charCodeAt(p - 1);
if (boundaryCc === LT || boundaryCc === DQUOTE || boundaryCc === SQUOTE || boundaryCc === EQ || boundaryCc === AMP) return false;
const textStart = lastIndexOfChar(oldHtml, GT, p - 1);
if (textStart === -1) return false;
const textEnd = oldHtml.indexOf("<", p);
if (textEnd === -1) return false;
if (textEnd < oldWinEnd) return false;
const newTextEnd = textEnd + (newHtml.length - oldHtml.length);
const oldText = oldHtml.slice(textStart + 1, textEnd);
const newText = newHtml.slice(textStart + 1, newTextEnd);
if (oldHtml.lastIndexOf("<!--kfb", textStart) !== -1) return false;
const textIdx = countTextNodesBefore(oldHtml, textStart + 1);
const targetNode = nthTextNodeDescendant(liveNode, textIdx);
if (targetNode === null) return false;
if (targetNode.nodeValue !== oldText) return false;
targetNode.nodeValue = newText;
const host = targetNode.parentNode;
if (host !== null && host.tagName === "TEXTAREA" && host !== document.activeElement) {
host.value = newText;
}
return true;
}
function containsDataMorphSkip(html) {
return html.indexOf("data-morph-skip") !== -1;
}
function isPureTextWindow(html, start, end) {
for (let i = start; i < end; i++) {
const cc = html.charCodeAt(i);
if (cc === LT || cc === GT || cc === DQUOTE || cc === SQUOTE || cc === AMP || cc === EQ) return false;
}
return true;
}
function lastIndexOfChar(html, target, beforeInclusive) {
for (let i = beforeInclusive; i >= 0; i--) {
if (html.charCodeAt(i) === target) return i;
}
return -1;
}
function countTextNodesBefore(html, beforePos) {
let count = 0;
let i = 0;
while (i < beforePos) {
if (html.charCodeAt(i) === LT) {
while (i < beforePos && html.charCodeAt(i) !== GT) i++;
i++;
} else {
const start = i;
while (i < beforePos && html.charCodeAt(i) !== LT) i++;
if (i > start) count++;
}
}
return count;
}
function nthTextNodeDescendant(root, n) {
let count = 0;
let result = null;
function walk(node) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (result !== null) return;
if (c.nodeType === TEXT_NODE2) {
if (count === n) {
result = c;
return;
}
count++;
} else if (c.nodeType === ELEMENT_NODE2) {
walk(c);
}
}
}
walk(root);
return result;
}
function parseOpeningTag(html, gtPos) {
if (html.charCodeAt(0) !== LT) return null;
let i = 1;
let end = gtPos;
if (i < end && html.charCodeAt(end - 1) === SLASH) end -= 1;
const nameStart = i;
while (i < end) {
const cc = html.charCodeAt(i);
if (isWhitespace(cc)) break;
i++;
}
const tagName = html.slice(nameStart, i);
if (tagName.length === 0) return null;
const attrs = /* @__PURE__ */ new Map();
while (i < end) {
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i >= end) break;
const aNameStart = i;
while (i < end) {
const cc = html.charCodeAt(i);
if (cc === EQ || isWhitespace(cc)) break;
i++;
}
const aName = html.slice(aNameStart, i);
if (aName.length === 0) return null;
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i < end && html.charCodeAt(i) === EQ) {
i++;
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i >= end) return null;
const q = html.charCodeAt(i);
if (q !== DQUOTE && q !== SQUOTE) return null;
i++;
const vStart = i;
while (i < end && html.charCodeAt(i) !== q) i++;
if (i >= end) return null;
attrs.set(aName, html.slice(vStart, i));
i++;
} else {
attrs.set(aName, "");
}
}
return { tagName, attrs };
}
function unescapeAttrValue(s) {
if (s.indexOf("&") === -1) return s;
return s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
}
function isUserAgentOwnedAttr2(tagNameUpper, name) {
return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG");
}
// src/list-reconcile-granular.ts
function reconcileGranular(binding, patches) {
const { liveParent } = binding;
const items = binding.items;
const focusSnap = captureFocus(liveParent);
let i = 0;
while (i < patches.length) {
const patch = patches[i];
if (patch.type === "replace") {
i += 1;
continue;
}
if (patch.type === "update") {
let runEnd = i + 1;
while (runEnd < patches.length && patches[runEnd].type === "update") {
runEnd += 1;
}
const runLen = runEnd - i;
if (runLen === 1) {
applySingleUpdate(liveParent, items, patch);
} else {
applyBulkUpdate(liveParent, items, patches, i, runEnd);
}
i = runEnd;
continue;
}
if (patch.type === "insert") {
let runEnd = i + 1;
while (runEnd < patches.length && patches[runEnd].type === "insert" && patches[runEnd].index === patches[runEnd - 1].index + 1) {
runEnd += 1;
}
const runLen = runEnd - i;
if (runLen === 1) {
applySingleInsert(liveParent, items, patch, endAnchor(binding));
} else {
applyBulkInsert(liveParent, items, patches, i, runEnd, endAnchor(binding));
}
i = runEnd;
continue;
}
if (patch.type === "remove") {
const entry = items[patch.index];
disposeRowBindings(entry.bindingDisposers);
liveParent.removeChild(entry.node);
items.splice(patch.index, 1);
i += 1;
continue;
}
if (patch.type === "move") {
const moved = items[patch.from];
let anchorIdx = patch.to;
if (patch.from < patch.to) anchorIdx += 1;
const anchor = anchorIdx < items.length ? items[anchorIdx].node : endAnchor(binding);
liveParent.insertBefore(moved.node, anchor);
items.splice(patch.from, 1);
items.splice(patch.to, 0, moved);
i += 1;
continue;
}
}
if (focusSnap !== null) restoreFocus(focusSnap);
if (items.length > 0) {
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
}
}
function applySingleInsert(liveParent, items, patch, tailAnchor) {
const { html } = patch;
const newNode = parseSingleRow(html, patch.index, liveParent);
const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor;
liveParent.insertBefore(newNode, anchor);
items.splice(patch.index, 0, {
ref: patch.item,
cacheKey: void 0,
html,
node: newNode,
bindings: patch.bindings,
// KF-294: wire the inserted row's fine-grained bindings to its new node.
bindingDisposers: wireRowIfBound(newNode, patch.bindings)
});
}
function wireRowIfBound(node, bindings) {
return bindings !== void 0 && bindings.length > 0 ? wireRowBindings(node, bindings) : void 0;
}
function applySingleUpdate(liveParent, items, patch) {
const { html } = patch;
const oldEntry = items[patch.index];
if (html === oldEntry.html) {
items[patch.index] = reuseBound(patch, html, oldEntry);
return;
}
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, html)) {
items[patch.index] = reuseBound(patch, html, oldEntry);
return;
}
const newNode = parseSingleRow(html, patch.index, liveParent);
applyParsedRowUpdate(liveParent, items, patch, html, newNode);
}
function applyParsedRowUpdate(liveParent, items, patch, html, newNode) {
const oldEntry = items[patch.index];
if (oldEntry.node.tagName === newNode.tagName) {
_morphElement(oldEntry.node, newNode);
items[patch.index] = reuseBound(patch, html, oldEntry);
} else {
disposeRowBindings(oldEntry.bindingDisposers);
liveParent.replaceChild(newNode, oldEntry.node);
items[patch.index] = {
ref: patch.item,
cacheKey: void 0,
html,
node: newNode,
bindings: patch.bindings,
bindingDisposers: wireRowIfBound(newNode, patch.bindings)
};
}
}
function reuseBound(patch, html, oldEntry) {
const kept = carryOrRewireRowBindings(
oldEntry.node,
oldEntry.bindings,
oldEntry.bindingDisposers,
patch.bindings
);
return {
ref: patch.item,
cacheKey: void 0,
html,
node: oldEntry.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
function applyBulkUpdate(liveParent, items, patches, start, end) {
const morphChanges = [];
for (let k = start; k < end; k++) {
const p = patches[k];
const oldEntry = items[p.index];
if (p.html === oldEntry.html) {
items[p.index] = reuseBound(p, p.html, oldEntry);
continue;
}
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, p.html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, p.html)) {
items[p.index] = reuseBound(p, p.html, oldEntry);
continue;
}
morphChanges.push({ patchIdx: k, html: p.html });
}
if (morphChanges.length === 0) return;
const { content, count } = parseRowTemplate(morphChanges.map((c) => c.html).join(""), liveParent);
if (count !== morphChanges.length) {
throw findOffendingChange(patches, morphChanges, liveParent);
}
const newNodes = collectTemplateChildren(content, morphChanges.length);
for (let k = 0; k < morphChanges.length; k++) {
const c = morphChanges[k];
const p = patches[c.patchIdx];
applyParsedRowUpdate(liveParent, items, p, c.html, newNodes[k]);
}
}
function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
const startIdx = patches[start].index;
const htmls = new Array(end - start);
for (let k = start; k < end; k++) {
htmls[k - start] = patches[k].html;
}
const { content, count } = parseRowTemplate(htmls.join(""), liveParent);
if (count !== htmls.length) {
throw findOffendingInsert(patches, start, htmls, liveParent);
}
const newNodes = collectTemplateChildren(content, end - start);
const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor;
liveParent.insertBefore(content, anchor);
const newEntries = new Array(end - start);
for (let k = 0; k < newEntries.length; k++) {
const p = patches[start + k];
newEntries[k] = {
ref: p.item,
cacheKey: void 0,
html: htmls[k],
node: newNodes[k],
bindings: p.bindings,
bindingDisposers: wireRowIfBound(newNodes[k], p.bindings)
// KF-294
};
}
items.splice(startIdx, 0, ...newEntries);
}
function findOffendingInsert(patches, start, htmls, liveParent) {
for (let i = 0; i < htmls.length; i++) {
if (parseRowTemplate(htmls[i], liveParent).count !== 1) {
return rowContractError(patches[start + i].index, htmls[i], liveParent);
}
}
return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug).");
}
function findOffendingChange(patches, changes, liveParent) {
for (const c of changes) {
if (parseRowTemplate(c.html, liveParent).count !== 1) {
return rowContractError(patches[c.patchIdx].index, c.html, liveParent);
}
}
return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug).");
}
// src/list-reconcile-inplace.ts
function tryInPlaceContentUpdate(binding, listSeg) {
const oldItems = binding.items;
const items = listSeg.items;
const n = items.length;
if (n === 0 || n !== oldItems.length) return false;
for (let i = 0; i < n; i++) {
if (items[i].ref !== oldItems[i].ref) return false;
}
const { liveParent } = binding;
const newRecord = new Array(n);
const focusSnap = captureFocus(liveParent);
for (let i = 0; i < n; i++) {
newRecord[i] = updateRowInPlace(liveParent, oldItems[i], items[i], i);
}
if (focusSnap !== null) restoreFocus(focusSnap);
binding.items = newRecord;
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
return true;
}
function updateRowInPlace(liveParent, old, ni, index) {
if (old.html === ni.html || tryAttributeOnlyFastPath(old.node, old.html, ni.html) || tryTextContentFastPath(old.node, old.html, ni.html)) {
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: old.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
const newNode = parseSingleRow(ni.html, index, liveParent);
if (old.node.tagName === newNode.tagName) {
_morphElement(old.node, newNode);
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: old.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
disposeRowBindings(old.bindingDisposers);
liveParent.replaceChild(newNode, old.node);
const fresh = carryOrRewireRowBindings(newNode, void 0, void 0, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: newNode,
bindings: fresh.bindings,
bindingDisposers: fresh.bindingDisposers
};
}
// src/list-reconcile-snapshot.ts
function reconcileSnapshot(binding, listSeg) {
if (tryInPlaceContentUpdate(binding, listSeg)) return;
const { liveParent } = binding;
const { newRecord, prevIdx, removedItems, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
const tailAnchor = endAnchor(binding);
buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent);
const focusSnap = captureFocus(liveParent);
removeOldNodes(liveParent, removedItems);
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
if (focusSnap !== null) restoreFocus(focusSnap);
binding.items = newRecord;
if (newRecord.length > 0) {
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
}
}
function classifyItems(oldItems, listSeg) {
const oldByRef = /* @__PURE__ */ new Map();
for (let i = 0; i < oldItems.length; i++) {
oldByRef.set(oldItems[i].ref, [oldItems[i], i]);
}
const newRecord = new Array(listSeg.items.length);
const prevIdx = new Array(listSeg.items.length);
const removedItems = [];
const freshIndices = [];
const freshHtmls = [];
for (let i = 0; i < listSeg.items.length; i++) {
const ni = listSeg.items[i];
const oi = oldByRef.get(ni.ref);
if (oi !== void 0) {
oldByRef.delete(ni.ref);
if (oi[0].html === ni.html) {
newRecord[i] = oi[0];
prevIdx[i] = oi[1];
continue;
}
removedItems.push(oi[0]);
}
newRecord[i] = {
// `node` placeholder is filled by `buildFreshNodes`; its parse-count
// check guarantees every fresh index gets a real element before use.
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: null,
bindings: ni.bindings
};
prevIdx[i] = -1;
freshIndices.push(i);
freshHtmls.push(ni.html);
}
for (const [, orphan] of oldByRef) removedItems.push(orphan[0]);
return { newRecord, prevIdx, removedItems, freshIndices, freshHtmls };
}
function buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent) {
if (freshHtmls.length === 0) return;
const { content, count } = parseRowTemplate(freshHtmls.join(""), liveParent);
if (count !== freshHtmls.length) {
throw findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent);
}
let node = content.firstElementChild;
for (const idx of freshIndices) {
const next = node.nextElementSibling;
const item = newRecord[idx];
item.node = node;
if (item.bindings !== void 0 && item.bindings.length > 0) {
item.bindingDisposers = wireRowBindings(item.node, item.bindings);
}
node = next;
}
}
function findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent) {
for (let i = 0; i < freshHtmls.length; i++) {
if (parseRowTemplate(freshHtmls[i], liveParent).count !== 1) {
return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html, liveParent);
}
}
return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug).");
}
function removeOldNodes(liveParent, removedItems) {
for (const item of removedItems) {
disposeRowBindings(item.bindingDisposers);
if (item.node.parentElement === liveParent) liveParent.removeChild(item.node);
}
}
function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) {
let nextSibling = tailAnchor;
for (let i = newRecord.length - 1; i >= 0; i--) {
const node = newRecord[i].node;
if (prevIdx[i] === -1 || !stable.has(i)) {
liveParent.insertBefore(node, nextSibling);
}
nextSibling = node;
}
}
function lis(arr) {
const tails = [];
const tailIdx = [];
const prev = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
const v = arr[i];
if (v === -1) {
prev[i] = -1;
continue;
}
let lo = 0;
let hi = tails.length;
while (lo < hi) {
const mid = lo + hi >> 1;
if (tails[mid] < v) lo = mid + 1;
else hi = mid;
}
prev[i] = lo > 0 ? tailIdx[lo - 1] : -1;
tails[lo] = v;
tailIdx[lo] = i;
}
const out = /* @__PURE__ */ new Set();
let k = tailIdx.length > 0 ? tailIdx[tailIdx.length - 1] : -1;
while (k !== -1) {
out.add(k);
k = prev[k];
}
return out;
}
// src/list-reconcile.ts
function reconcileList(binding, listSeg) {
if (listSeg.patches !== void 0 && binding.items.length > 0) {
reconcileGranular(binding, listSeg.patches);
return;
}
reconcileSnapshot(binding, listSeg);
}
// src/mount.ts
var MOUNTED_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.mounted");
var NESTED_MOUNT_MSG = "mount: rootEl is already inside (or contains) a mounted tree. kerf supports one mount per tree \u2014 compose with plain functions that return JSX instead of nesting mounts.";
function isMounted(el) {
return el[MOUNTED_MARKER] === true;
}
function setMounted(el, on) {
if (on) {
el[MOUNTED_MARKER] = true;
} else {
delete el[MOUNTED_MARKER];
}
}
function describeEl(el) {
const tag = el.tagName.toLowerCase();
const id = el.id ? `#${el.id}` : "";
return `<${tag}${id}>`;
}
function assertNotInsideMountedTree(rootEl) {
if (isMounted(rootEl)) {
throw new Error(
`mount: ${describeEl(rootEl)} is already mounted. Call the disposer returned by the first mount() before mounting again. kerf supports one mount per element \u2014 compose with plain functions that return JSX instead of nesting mounts.`
);
}
let ancestor = rootEl.parentElement;
while (ancestor !== null) {
if (isMounted(ancestor)) throw new Error(NESTED_MOUNT_MSG);
ancestor = ancestor.parentElement;
}
const stack = [];
for (let i = 0; i < rootEl.children.length; i++) stack.push(rootEl.children[i]);
while (stack.length > 0) {
const cur = stack.pop();
if (isMounted(cur)) throw new Error(NESTED_MOUNT_MSG);
for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
}
}
function mount(rootEl, render) {
if (rootEl == null) {
throw new Error(
'mount: rootEl is null/undefined \u2014 pass the live element, e.g. mount(document.getElementById("app")!, render). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say HTMLElement.'
);
}
const owner = rootEl.ownerDocument;
if (owner !== document) {
if (owner.defaultView === null) document.adoptNode(rootEl);
}
assertNotInsideMountedTree(rootEl);
setMounted(rootEl, true);
const listenerWarnObserver = devHooks.listenerRebuild?.(rootEl) ?? null;
const bindings = /* @__PURE__ */ new Map();
const renderCtx = {
counter: 0,
caches: /* @__PURE__ */ new Map(),
bindingCounts: /* @__PURE__ */ new Map(),
bindingSources: /* @__PURE__ */ new Map(),
keysThisRender: /* @__PURE__ */ new Set(),
shiftCandidates: [],
warnedShiftIds: /* @__PURE__ */ new Set(),
rebuiltLists: /* @__PURE__ */ new Set()
};
const bindingCtx = newBindingContext();
let bindingDisposers = [];
let prevWiredBindings = [];
let isFirst = true;
let prevStaticHtml = "";
const valueOnlyWarnCtx = { warned: false };
const runRenderPass = () => {
renderCtx.counter = 0;
renderCtx.keysThisRender.clear();
renderCtx.shiftCandidates.length = 0;
bindingCtx.counter = 0;
bindingCtx.list = [];
_setRenderContext(renderCtx);
_setBindingContext(bindingCtx);
try {
return render();
} finally {
_setRenderContext(null);
_setBindingContext(null);
}
};
const disposeEffect = effect(() => {
let result = runRenderPass();
const countChanged = renderCtx.previousCallCount !== void 0 && renderCtx.previousCallCount !== renderCtx.counter;
if (countChanged) {
for (const id of renderCtx.shiftCandidates) {
if (renderCtx.warnedShiftIds.has(id)) continue;
renderCtx.warnedShiftIds.add(id);
devHooks.listIdShift?.(id);
}
_resetCallOrderListState(renderCtx);
result = runRenderPass();
}
let segment = resultToSegment(result);
if (isFirst) {
runFirstRender(rootEl, segment, bindings);
prevStaticHtml = flattenWithoutListItems(segment);
devHooks.parserRepair?.(prevStaticHtml);
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
isFirst = false;
} else {
let nextStaticHtml = runSubsequentRender(
rootEl,
segment,
bindings,
renderCtx,
prevStaticHtml,
valueOnlyWarnCtx
);
if (anyRebuiltListIsGranular(segment, renderCtx.rebuiltLists)) {
for (const id of renderCtx.rebuiltLists) renderCtx.bindingCounts.delete(id);
result = runRenderPass();
segment = resultToSegment(result);
nextStaticHtml = runSubsequentRender(
rootEl,
segment,
bindings,
renderCtx,
prevStaticHtml,
valueOnlyWarnCtx
);
}
if (nextStaticHtml !== prevStaticHtml) {
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
} else {
devHooks.staleBinding?.(prevWiredBindings, bindingCtx.list);
}
prevStaticHtml = nextStaticHtml;
}
const expectedCounts = devHooks.listInvariantsEnabled?.() === true ? /* @__PURE__ */ new Map() : null;
for (const listSeg of collectLists(segment).values()) {
const binding = bindings.get(listSeg.id);
if (binding === void 0) {
throw new Error(
"mount: an each() list appeared in the render output but its marker never reached the live DOM. The most common cause is an each() introduced inside a data-morph-skip subtree on a re-render \u2014 the morph leaves that subtree untouched, so the list can never bind. Move the each() outside the skipped subtree, or remove data-morph-skip from its ancestor."
);
}
reconcileList(binding, listSeg);
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
renderCtx.bindingSources.set(listSeg.id, listSeg.source);
expectedCounts?.set(
listSeg.id,
listSeg.patches !== void 0 && listSeg.source !== void 0 ? listSeg.source.value.length : listSeg.items.length
);
}
renderCtx.previousCallCount = renderCtx.counter;
devHooks.listInvariants?.(rootEl, bindings, expectedCounts ?? void 0);
});
return () => {
disposeEffect();
for (const d of bindingDisposers) d();
bindingDisposers = [];
for (const b of bindings.values()) {
for (const item of b.items) disposeRowBindings(item.bindingDisposers);
}
listenerWarnObserver?.disconnect();
setMounted(rootEl, false);
};
}
function runFirstRender(rootEl, segment, bindings) {
rootEl.innerHTML = flatten(segment, true);
bindListsFromMarkers(rootEl, segment, bindings, true);
}
function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml, valueOnlyWarnCtx) {
renderCtx.rebuiltLists.clear();
const currentStaticHtml = flattenWithoutListItems(segment);
if (currentStaticHtml === prevStaticHtml) {
return prevStaticHtml;
}
devHooks.valueOnlyRerender?.(prevStaticHtml, currentStaticHtml, valueOnlyWarnCtx);
cleanupOrphanBindings(segment, bindings, renderCtx);
const template = rootEl.cloneNode(false);
template.innerHTML = currentStaticHtml;
morph(rootEl, template, collectOwnedItems(bindings));
bindListsFromMarkers(rootEl, segment, bindings, false, renderCtx.rebuiltLists);
return currentStaticHtml;
}
function coerceRenderResult(result) {
if (result === null || result === void 0) return "";
if (result === false || result === true) return "";
return String(result);
}
function resultToSegment(result) {
return isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: coerceRenderResult(result) };
}
function anyRebuiltListIsGranular(segment, rebuilt) {
if (rebuilt.size === 0) return false;
const lists = collectLists(segment);
for (const id of rebuilt) {
if (lists.get(id)?.patches !== void 0) return true;
}
return false;
}
function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems, rebuiltLists) {
const lists = collectLists(segment);
const found = [];
collectComments(rootEl, found);
for (const marker of found) {
if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
const id = marker.data.slice(LIST_MARKER_PREFIX.length);
const existing = bindings.get(id);
if (existing !== void 0) {
if (existing.marker === marker && rootEl.contains(existing.marker)) continue;
for (const item of existing.items) {
disposeRowBindings(item.bindingDisposers);
if (rootEl.contains(item.node)) {
item.node.parentElement?.removeChild(item.node);
}
}
bindings.delete(id);
rebuiltLists?.add(id);
devHooks.listRebind?.(id, marker.parentElement);
}
const listSeg = lists.get(id);
const liveParent = marker.parentElement;
const items = [];
if (inlinedItems) {
let next = marker.nextElementSibling;
for (let i = 0; i < listSeg.items.length && next !== null; i++) {
validateInlinedRowMatch(listSeg.items[i].html, i, next, liveParent);
const rowBindings = listSeg.items[i].bindings;
const bound = {
ref: listSeg.items[i].ref,
cacheKey: listSeg.items[i].cacheKey,
html: listSeg.items[i].html,
node: next,
bindings: rowBindings
};
if (rowBindings !== void 0 && rowBindings.length > 0) {
bound.bindingDisposers = wireRowBindings(next, rowBindings);
}
items.push(bound);
next = next.nextElementSibling;
}
}
const binding = { liveParent, items, marker };
if (items.length > 0) {
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
}
devHooks.eachInMorphSkip?.(id, liveParent, rootEl);
bindings.set(id, binding);
}
}
function validateInlinedRowMatch(expectedHtml, index, boundEl, liveParent) {
if (boundEl.outerHTML === expectedHtml) return;
const { content, count } = parseRowTemplate(expectedHtml, liveParent);
if (count !== 1) throw rowContractError(index, expectedHtml, liveParent);
const expectedTag = content.firstElementChild.tagName;
if (boundEl.tagName !== expectedTag) throw rowStructureError(index, boundEl.tagName, expectedTag);
}
function rowStructureError(index, gotTag, wantTag) {
const got = gotTag.toLowerCase();
const want = wantTag.toLowerCase();
return new Error(
`each(): row ${index} renders <${want}>, but the HTML parser wrapped the rows in <${got}> \u2014 so kerf cannot bind one row per element. This happens when an each() of <${want}> sits directly inside a table: the parser inserts <${got}> around the whole run. Put the each() inside an explicit <${got}> (e.g. <table><${got}>{each(...)}</${got}></table>) so the rows are the direct children kerf binds.`
);
}
function collectOwnedItems(bindings) {
const owned = /* @__PURE__ */ new Set();
for (const b of bindings.values()) {
for (const item of b.items) owned.add(item.node);
}
return owned;
}
function cleanupOrphanBindings(segment, bindings, renderCtx) {
const liveIds = collectLists(segment);
for (const [id, binding] of bindings) {
if (liveIds.has(id)) continue;
for (const item of binding.items) {
disposeRowBindings(item.bindingDisposers);
if (item.node.parentElement !== null) {
item.node.parentElement.removeChild(item.node);
}
}
if (binding.marker.parentElement !== null) {
binding.marker.parentElement.removeChild(binding.marker);
}
bindings.delete(id);
renderCtx.bindingCounts.delete(id);
renderCtx.bindingSources.delete(id);
renderCtx.caches.delete(id);
}
}
function collectComments(node, out) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === Node.COMMENT_NODE) out.push(c);
else if (c.nodeType === Node.ELEMENT_NODE) collectComments(c, out);
}
}
export { each, morph, mount };
//# sourceMappingURL=chunk-LKWAKC2X.js.map
//# sourceMappingURL=chunk-LKWAKC2X.js.map

Sorry, the diff of this file is too big to display

import { effect, isSignal } from './chunk-3APBEVHF.js';
import { flatten, wrapWithTags, mergeChildSegments } from './chunk-GY4XV2UV.js';
import { devHooks } from './chunk-VVDJLWMP.js';
// src/utils/syncFormProp.ts
function syncFormProp(el, name, value, present) {
const tag = el.tagName;
if (name === "checked") {
if (tag === "INPUT") el.checked = present;
} else if (name === "value") {
if (tag === "INPUT" && el !== document.activeElement) {
el.value = present ? value : "";
}
} else if (name === "selected") {
if (tag === "OPTION") el.selected = present;
}
}
// src/utils/urlScreen.ts
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction", "action", "data"]);
var DANGEROUS_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript"]);
var INERT_JAVASCRIPT_URLS = /* @__PURE__ */ new Set([
"javascript:",
"javascript:;",
"javascript:void(0)",
"javascript:void(0);",
"javascript:void 0",
"javascript:void 0;"
]);
var CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
function normalizeUrl(value) {
return value.replace(CONTROL_CHARS, "").replace(/^\s+/, "");
}
function schemeOf(normalized) {
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(normalized);
return m ? m[1].toLowerCase() : null;
}
function isDangerousDataUrl(normalized) {
const media = /^data:([^;,]*)/.exec(normalized.toLowerCase())?.[1].trim() ?? "";
if (media === "" || media === "text/plain" || media === "text/css") return false;
if (media === "image/svg+xml") return true;
if (media.startsWith("image/")) return false;
if (media.startsWith("font/") || media.startsWith("application/font")) return false;
if (media.startsWith("audio/") || media.startsWith("video/")) return false;
return true;
}
function isDangerousUrlValue(name, value) {
if (!URL_ATTRS.has(name)) return false;
const normalized = normalizeUrl(value);
const scheme = schemeOf(normalized);
if (scheme === null) return false;
if (DANGEROUS_SCHEMES.has(scheme)) {
return !INERT_JAVASCRIPT_URLS.has(normalized.trimEnd().toLowerCase());
}
return scheme === "data" && isDangerousDataUrl(normalized);
}
function dangerousUrlWarning(name, value) {
return `dropped dangerous URL value for ${name}=${JSON.stringify(value.slice(0, 80))}. kerf blocks javascript:, vbscript:, and script-executing data: URLs (text/html, image/svg+xml, xml) in href/src/data/formaction/action/xlink:href by default. Wrap in raw() if this is intentional (e.g. bookmarklets), or sanitize upstream.`;
}
function reportDangerousUrl(context2, name, value) {
const message = `${context2}: ${dangerousUrlWarning(name, value)}`;
devHooks.urlScreenThrow?.(message);
console.warn(message);
}
// src/bindings.ts
var BIND_ATTR = "data-kfb";
var TEXT_MARKER_PREFIX = "kfb:";
var BIND_ATTR_ROW = "data-kfbrow";
var ROW_TEXT_PREFIX = "kfbr:";
var context = null;
var rowSink = null;
var rowCounter = 0;
var NO_DISPOSERS = Object.freeze([]);
function newBindingContext() {
return { counter: 0, list: [] };
}
function _setBindingContext(c) {
context = c;
}
function captureRowBindings(renderRow) {
const prevSink = rowSink;
const prevCounter = rowCounter;
rowSink = [];
rowCounter = 0;
try {
const html = renderRow();
return { html, bindings: rowSink };
} finally {
rowSink = prevSink;
rowCounter = prevCounter;
}
}
function bindAttr(attr, signal) {
if (rowSink !== null) {
const id = `a${rowCounter++}`;
rowSink.push({ kind: "attr", id, attr, signal });
return id;
}
if (context !== null) {
const id = `a${context.counter++}`;
context.list.push({ kind: "attr", id, attr, signal });
return id;
}
return null;
}
function bindMarkerAttr() {
return rowSink !== null ? BIND_ATTR_ROW : BIND_ATTR;
}
function bindText(signal) {
if (rowSink !== null) {
const id = `t${rowCounter++}`;
rowSink.push({ kind: "text", id, signal });
return `<!--${ROW_TEXT_PREFIX}${id}-->`;
}
if (context !== null) {
const id = `t${context.counter++}`;
context.list.push({ kind: "text", id, signal });
return `<!--${TEXT_MARKER_PREFIX}${id}-->`;
}
return null;
}
function wireBindings(rootEl, ctx, prevDisposers) {
for (const d of prevDisposers) d();
if (ctx.list.length === 0) return NO_DISPOSERS;
const disposers = [];
wireInto(rootEl, ctx.list, disposers);
return disposers;
}
function wireRowBindings(rowNode, bindings) {
const disposers = new Array(bindings.length);
const rootIds = rowNode.getAttribute(BIND_ATTR_ROW);
let rootIdSet = null;
let descIndex = null;
let textMarkers = null;
for (let i = 0; i < bindings.length; i++) {
const b = bindings[i];
if (b.kind === "attr") {
let onRoot = false;
if (rootIds !== null) {
if (rootIds === b.id) {
onRoot = true;
} else if (rootIds.indexOf(",") !== -1) {
rootIdSet ??= new Set(rootIds.split(","));
onRoot = rootIdSet.has(b.id);
}
}
let el;
if (onRoot) {
el = rowNode;
} else {
descIndex ??= indexAttrEls(rowNode, BIND_ATTR_ROW);
el = descIndex.get(b.id);
}
if (el === void 0) continue;
disposers[i] = attachAttrEffect(el, b.attr, b.signal);
} else {
if (textMarkers === null) {
textMarkers = /* @__PURE__ */ new Map();
collectComments(rowNode, ROW_TEXT_PREFIX, textMarkers);
}
const marker = textMarkers.get(b.id);
if (marker === void 0) continue;
disposers[i] = attachTextEffect(marker, b.signal);
}
}
return disposers;
}
function disposeRowBindings(disposers) {
if (disposers === void 0) return;
for (const d of disposers) d();
}
function carryOrRewireRowBindings(node, oldBindings, oldDisposers, newBindings) {
const oldLen = oldBindings === void 0 ? 0 : oldBindings.length;
const newLen = newBindings === void 0 ? 0 : newBindings.length;
if (oldLen === newLen) {
let same = true;
for (let i = 0; i < newLen; i++) {
if (oldBindings[i].signal !== newBindings[i].signal) {
same = false;
break;
}
}
if (same) return { bindings: oldBindings, bindingDisposers: oldDisposers };
}
disposeRowBindings(oldDisposers);
return {
bindings: newBindings,
bindingDisposers: newLen > 0 ? wireRowBindings(node, newBindings) : void 0
};
}
function wireInto(scope, bindings, disposers) {
const attrEls = indexAttrEls(scope, BIND_ATTR);
const textMarkers = /* @__PURE__ */ new Map();
collectComments(scope, TEXT_MARKER_PREFIX, textMarkers);
for (const b of bindings) {
if (b.kind === "attr") {
const el = attrEls.get(b.id);
if (el === void 0) continue;
disposers.push(attachAttrEffect(el, b.attr, b.signal));
} else {
const marker = textMarkers.get(b.id);
if (marker === void 0) continue;
disposers.push(attachTextEffect(marker, b.signal));
}
}
}
function indexAttrEls(scope, attrName) {
const map = /* @__PURE__ */ new Map();
for (const el of scope.querySelectorAll(`[${attrName}]`)) {
for (const id of el.getAttribute(attrName).split(",")) map.set(id, el);
}
return map;
}
function attachAttrEffect(el, attr, signal) {
return effect(() => setBoundAttr(el, attr, signal.value));
}
var insertedTextNodes = /* @__PURE__ */ new WeakMap();
function boundTextNodeOf(marker) {
const t = insertedTextNodes.get(marker);
return t !== void 0 && marker.nextSibling === t ? t : null;
}
function attachTextEffect(marker, signal) {
let text = insertedTextNodes.get(marker);
if (text === void 0 || marker.nextSibling !== text) {
text = marker.ownerDocument.createTextNode("");
marker.parentNode.insertBefore(text, marker.nextSibling);
insertedTextNodes.set(marker, text);
}
const node = text;
return effect(() => {
node.data = coerceText(signal.value);
});
}
function setBoundAttr(el, name, value) {
if (value == null || value === false) {
el.removeAttribute(name);
syncFormProp(el, name, "", false);
return;
}
if (value === true) {
el.setAttribute(name, "");
syncFormProp(el, name, "", true);
return;
}
if (isSafeHtmlValue(value)) {
el.setAttribute(name, value.__html);
syncFormProp(el, name, value.__html, true);
return;
}
const str = String(value);
if (isDangerousUrlValue(name, str)) {
reportDangerousUrl("kerf binding", name, str);
el.removeAttribute(name);
return;
}
el.setAttribute(name, str);
syncFormProp(el, name, str, true);
}
var SAFE_HTML_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
function isSafeHtmlValue(v) {
return typeof v === "object" && v !== null && v[SAFE_HTML_BRAND] === true;
}
function coerceText(value) {
if (value == null || typeof value === "boolean") return "";
return String(value);
}
function collectComments(node, prefix, out) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === Node.COMMENT_NODE) {
const data = c.data;
if (data.startsWith(prefix)) out.set(data.slice(prefix.length), c);
} else if (c.nodeType === Node.ELEMENT_NODE) {
collectComments(c, prefix, out);
}
}
}
// src/utils/escapeHtml.ts
function escapeHtml(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function escapeAttr(str) {
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// src/utils/jsx-attr-aliases.ts
var ATTR_ALIASES = {
// HTML attributes
className: "class",
htmlFor: "for",
httpEquiv: "http-equiv",
acceptCharset: "accept-charset",
accessKey: "accesskey",
autoCapitalize: "autocapitalize",
autoComplete: "autocomplete",
autoFocus: "autofocus",
autoPlay: "autoplay",
colSpan: "colspan",
contentEditable: "contenteditable",
crossOrigin: "crossorigin",
dateTime: "datetime",
defaultChecked: "checked",
defaultSelected: "selected",
defaultValue: "value",
encType: "enctype",
formAction: "formaction",
formEncType: "formenctype",
formMethod: "formmethod",
formNoValidate: "formnovalidate",
formTarget: "formtarget",
hrefLang: "hreflang",
inputMode: "inputmode",
maxLength: "maxlength",
minLength: "minlength",
noModule: "nomodule",
noValidate: "novalidate",
readOnly: "readonly",
referrerPolicy: "referrerpolicy",
rowSpan: "rowspan",
spellCheck: "spellcheck",
srcDoc: "srcdoc",
srcLang: "srclang",
srcSet: "srcset",
tabIndex: "tabindex",
useMap: "usemap",
// SVG presentation attributes (camelCase → kebab-case)
strokeWidth: "stroke-width",
strokeLinecap: "stroke-linecap",
strokeLinejoin: "stroke-linejoin",
strokeDasharray: "stroke-dasharray",
strokeDashoffset: "stroke-dashoffset",
strokeMiterlimit: "stroke-miterlimit",
strokeOpacity: "stroke-opacity",
fillOpacity: "fill-opacity",
fillRule: "fill-rule",
clipPath: "clip-path",
clipRule: "clip-rule",
colorInterpolation: "color-interpolation",
colorInterpolationFilters: "color-interpolation-filters",
floodColor: "flood-color",
floodOpacity: "flood-opacity",
lightingColor: "lighting-color",
stopColor: "stop-color",
stopOpacity: "stop-opacity",
shapeRendering: "shape-rendering",
imageRendering: "image-rendering",
textRendering: "text-rendering",
pointerEvents: "pointer-events",
vectorEffect: "vector-effect",
paintOrder: "paint-order",
// SVG text/font attributes
fontFamily: "font-family",
fontSize: "font-size",
fontStyle: "font-style",
fontVariant: "font-variant",
fontWeight: "font-weight",
fontStretch: "font-stretch",
textAnchor: "text-anchor",
textDecoration: "text-decoration",
dominantBaseline: "dominant-baseline",
alignmentBaseline: "alignment-baseline",
baselineShift: "baseline-shift",
letterSpacing: "letter-spacing",
wordSpacing: "word-spacing",
writingMode: "writing-mode",
// SVG marker attributes
markerStart: "marker-start",
markerMid: "marker-mid",
markerEnd: "marker-end",
// SVG xlink (legacy but still used)
xlinkHref: "xlink:href",
xlinkShow: "xlink:show",
xlinkActuate: "xlink:actuate",
xlinkType: "xlink:type",
xlinkRole: "xlink:role",
xlinkTitle: "xlink:title",
xlinkArcrole: "xlink:arcrole",
xmlBase: "xml:base",
xmlLang: "xml:lang",
xmlSpace: "xml:space",
xmlnsXlink: "xmlns:xlink"
};
// src/jsx-runtime.ts
var SAFE_HTML_BRAND2 = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
var SafeHtml = class {
__html;
__segment;
// Branded so `isSafeHtml()` recognizes instances from any copy of this module.
[SAFE_HTML_BRAND2] = true;
constructor(input) {
if (typeof input === "string") {
this.__segment = { kind: "static", html: input };
this.__html = input;
} else {
this.__segment = input;
this.__html = flatten(input, false);
}
}
toString() {
return this.__html;
}
};
function isSafeHtml(value) {
return typeof value === "object" && value !== null && value[SAFE_HTML_BRAND2] === true;
}
function raw(html) {
return new SafeHtml(html);
}
function listSafeHtml(id, items, source) {
return new SafeHtml({ kind: "list", id, items, source });
}
function granularListSafeHtml(id, items, patches, source) {
return new SafeHtml({ kind: "list", id, items, patches, source });
}
var VOID_TAGS = /* @__PURE__ */ new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"source",
"track",
"wbr"
]);
function toSegment(child) {
if (child == null || typeof child === "boolean") return { kind: "static", html: "" };
if (isSignal(child)) {
const marker = bindText(child);
if (marker !== null) return { kind: "static", html: marker };
const v = child.value;
return { kind: "static", html: v == null || typeof v === "boolean" ? "" : escapeHtml(String(v)) };
}
if (isSafeHtml(child)) {
return child.__segment ?? { kind: "static", html: child.__html };
}
if (typeof child === "string") return { kind: "static", html: escapeHtml(child) };
if (typeof child === "number") return { kind: "static", html: String(child) };
if (Array.isArray(child)) return mergeChildSegments(child.map(toSegment));
const maybeNode = child;
if (typeof maybeNode === "object" && maybeNode !== null && ("nodeType" in maybeNode || "outerHTML" in maybeNode)) {
throw new Error(
"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."
);
}
throw new Error(
`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).`
);
}
function describeValue(v) {
if (Array.isArray(v)) return "array";
if (typeof v === "object" && v !== null) {
const ctor = v.constructor?.name;
return ctor && ctor !== "Object" ? `object (${ctor})` : "object";
}
return typeof v;
}
var SAFE_ATTR_NAME = /^[A-Za-z_:][\w.:-]*$/;
function assertEmittableAttrName(key, name, isFn) {
if (/^on[a-z]/i.test(name)) {
if (isFn) {
throw new Error(
`JSX: inline event handlers like ${key}={fn} are not supported by kerf's JSX \u2192 HTML-string runtime. Use event delegation from the mount root instead:
delegate(rootEl, 'click', '[data-action="..."]', (evt, target) => { ... });
<button data-action="...">click</button>
See docs/5-event-delegation.md for the tier-1/tier-2/tier-3 model.`
);
}
throw new Error(
`JSX: event-handler attribute ${JSON.stringify(key)} is not allowed \u2014 an \`on*\` attribute (whether a string emitted into HTML or a signal bound via setAttribute) installs a live inline handler, an XSS vector. kerf uses event delegation: delegate(rootEl, 'click', '[data-action="..."]', handler). See docs/5-event-delegation.md.`
);
}
if (!SAFE_ATTR_NAME.test(name)) {
throw new Error(
`JSX: invalid attribute name ${JSON.stringify(key)}. Attribute names must be a letter/underscore/colon followed by letters, digits, or "_.:-" (e.g. class, data-id, aria-label, xlink:href). This usually means an untrusted object was spread into JSX ({...obj}) with attacker-controlled keys \u2014 validate keys first.`
);
}
}
function renderAttr(key, value) {
return renderAttrNamed(key, ATTR_ALIASES[key] ?? key, value);
}
function renderAttrNamed(key, name, value) {
if (value == null || value === false) return "";
assertEmittableAttrName(key, name, typeof value === "function");
if (value === true) return ` ${name}`;
let strValue;
if (isSafeHtml(value)) {
strValue = value.__html;
} else if (typeof value === "number") {
strValue = String(value);
} else if (typeof value === "string") {
if (isDangerousUrlValue(name, value)) {
reportDangerousUrl("JSX", name, value);
return "";
}
strValue = escapeAttr(value);
} else {
throw new Error(
`JSX: unsupported value for attribute "${key}" \u2014 got ${describeValue(value)}. Attribute values must be string, number, boolean, null, undefined, or SafeHtml. Did you mean to read .value off a Signal, or stringify the object first?`
);
}
return ` ${name}="${strValue}"`;
}
function jsx(tag, props) {
if (typeof tag === "function") return tag(props);
const { children, ...attrs } = props;
let attrStr = "";
let bindIds = null;
for (const [k, v] of Object.entries(attrs)) {
if (isSignal(v)) {
const name = ATTR_ALIASES[k] ?? k;
assertEmittableAttrName(k, name, false);
const id = bindAttr(name, v);
if (id !== null) {
(bindIds ??= []).push(id);
continue;
}
attrStr += renderAttr(k, v.value);
continue;
}
attrStr += renderAttr(k, v);
}
if (bindIds !== null) attrStr += ` ${bindMarkerAttr()}="${bindIds.join(",")}"`;
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
const childSegment = children != null ? toSegment(children) : { kind: "static", html: "" };
return new SafeHtml(wrapWithTags(childSegment, `<${tag}${attrStr}>`, `</${tag}>`));
}
function Fragment({ children }) {
return new SafeHtml(children != null ? toSegment(children) : { kind: "static", html: "" });
}
function _toSegment(child) {
return toSegment(child);
}
function _renderAttrVerbatim(name, value) {
return renderAttrNamed(name, name, value);
}
export { Fragment, ROW_TEXT_PREFIX, SafeHtml, TEXT_MARKER_PREFIX, _renderAttrVerbatim, _setBindingContext, _toSegment, assertEmittableAttrName, bindAttr, bindMarkerAttr, boundTextNodeOf, captureRowBindings, carryOrRewireRowBindings, disposeRowBindings, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, newBindingContext, raw, syncFormProp, wireBindings, wireRowBindings };
//# sourceMappingURL=chunk-SUPUPSBE.js.map
//# sourceMappingURL=chunk-SUPUPSBE.js.map

Sorry, the diff of this file is too big to display

+1
-1

@@ -1,2 +0,2 @@

import { _toSegment, assertEmittableAttrName, bindAttr, _renderAttrVerbatim, SafeHtml, bindMarkerAttr } from './chunk-FSAQR6IU.js';
import { _toSegment, assertEmittableAttrName, bindAttr, _renderAttrVerbatim, SafeHtml, bindMarkerAttr } from './chunk-SUPUPSBE.js';
import { isSignal } from './chunk-3APBEVHF.js';

@@ -3,0 +3,0 @@ import { mergeChildSegments } from './chunk-GY4XV2UV.js';

@@ -5,3 +5,3 @@ export { A as AttrSpec, a as attr } from './attrSelector-Cmu2ZoGO.js';

import { SafeHtml } from './jsx-runtime.js';
export { Fragment, isSafeHtml, raw, trustedRaw } from './jsx-runtime.js';
export { Fragment, isSafeHtml, raw } from './jsx-runtime.js';
export { M as MountResult, m as mount } from './mount-Bo2qOx25.js';

@@ -8,0 +8,0 @@ import { Signal } from '@preact/signals-core';

@@ -1,2 +0,2 @@

export { each, morph, mount } from './chunk-4MY2656S.js';
export { each, morph, mount } from './chunk-LKWAKC2X.js';
export { defineStore, resetAllStores } from './chunk-SAYPJ6XR.js';

@@ -7,3 +7,3 @@ export { attr } from './chunk-U32TFTGZ.js';

import './chunk-YHH7OUFA.js';
export { Fragment, SafeHtml, isSafeHtml, raw, trustedRaw } from './chunk-FSAQR6IU.js';
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-SUPUPSBE.js';
export { batch, computed, effect, signal } from './chunk-3APBEVHF.js';

@@ -10,0 +10,0 @@ import './chunk-GY4XV2UV.js';

@@ -1163,16 +1163,28 @@ import { ReadonlySignal } from '@preact/signals-core';

declare function isSafeHtml(value: unknown): value is SafeHtml;
/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */
declare function raw(html: string): SafeHtml;
/**
* `trustedRaw(html)` — identical to {@link raw} at runtime, but names your intent:
* "this dynamic value is server-trusted, inject it verbatim." The
* `kerfjs/no-raw-with-dynamic-arg` lint rule flags a `raw()` with a NON-literal
* argument (unsanitized user input is the common XSS mistake) but leaves
* `trustedRaw()` alone — so a CSRF token, a trusted `<script src>`, or a
* server-issued id can be injected without scattering `eslint-disable` comments.
* Inject a pre-escaped HTML string verbatim, bypassing kerf's auto-escaping.
*
* It is NOT a sanitizer — it bypasses escaping exactly like `raw()`. Only pass
* values you control (server output, config, hard-coded), never raw user input.
* **Reach for this rarely.** kerf escapes automatically everywhere else, so a lot
* of `raw()` in a codebase is usually a sign the wrong tool is being used — the
* common cases have a safer, first-class answer:
* - Interpolating dynamic text/attributes? Plain JSX (`<p>{value}</p>`,
* `class={sig}`) already escapes it; you don't need `raw()`.
* - Composing markup? Build a {@link SafeHtml} the normal way — a JSX expression,
* the `html` tagged template (`kerfjs/html`), `each()`, or a component function
* that returns JSX. Those all produce trusted `SafeHtml` without hand-writing
* an HTML string.
* - A genuinely trusted, pre-escaped **dynamic** value (server output, config, a
* hard-coded string)? That's the one legitimate use. The
* `kerfjs/no-raw-with-dynamic-arg` lint rule flags a `raw()` whose argument is
* not a literal — because unsanitized user input is the canonical XSS mistake —
* so acknowledge it with a `// eslint-disable-next-line
* kerfjs/no-raw-with-dynamic-arg` at that call. That explicit override is the
* single sanctioned way to say "I've reviewed this; it's trusted," and it
* leaves a searchable audit trail.
*
* `raw()` is NOT a sanitizer — it does no escaping. For user-controlled input,
* sanitize first (`raw(DOMPurify.sanitize(marked(userMarkdown)))`) or, better,
* render it through escaping JSX instead.
*/
declare function trustedRaw(html: string): SafeHtml;
declare function raw(html: string): SafeHtml;
/**

@@ -1252,2 +1264,2 @@ * Internal: build a `SafeHtml` representing a list segment. Used by

export { type AttrLike, type AttrValue, type DataAriaAttrs, Fragment, JSX, type KerfBaseAttrs, type KerfCustomElement, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw, trustedRaw };
export { type AttrLike, type AttrValue, type DataAriaAttrs, Fragment, JSX, type KerfBaseAttrs, type KerfCustomElement, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw };

@@ -1,2 +0,2 @@

export { Fragment, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw, trustedRaw } from './chunk-FSAQR6IU.js';
export { Fragment, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw } from './chunk-SUPUPSBE.js';
import './chunk-3APBEVHF.js';

@@ -3,0 +3,0 @@ import './chunk-GY4XV2UV.js';

@@ -24,2 +24,28 @@ import { M as MountResult } from './mount-Bo2qOx25.js';

};
/**
* The virtualization height model:
* - **`number`** — every row is this fixed pixel height (O(1) windowing).
* - **`(item, index) => number`** — app-declared **variable** heights, derived
* purely from the item and its index.
* - **`{ estimate }`** — **measured** heights: kerf uses `estimate` for a row
* until the app reports its real height through {@link BindListHandle.setHeight}
* (or the `observeRowHeights` helper). See `docs/17-list-virtualization.md`.
*/
type RowHeight<T> = number | ((item: T, index: number) => number) | {
estimate: number | ((item: T, index: number) => number);
};
/**
* The value {@link bindList} returns: a disposer you call to tear the list down,
* augmented with `setHeight` for the **measured** virtualization mode.
*/
type BindListHandle = (() => void) & {
/**
* Report a row's real pixel height (measured after layout) for
* `virtualize: { rowHeight: { estimate } }` lists. Keyed by the list `key`, so
* a report survives reorders. kerf recomputes the window and, if the row sits
* ABOVE the viewport, anchor-corrects `scrollTop` so content doesn't jump.
* A no-op for fixed / declared-height lists and for unknown keys.
*/
setHeight: (key: ListKey, height: number) => void;
};
/** Options for {@link bindList}. */

@@ -46,9 +72,33 @@ interface BindListOptions<T> {

/**
* Turn on viewport virtualization. `rowHeight` is the fixed pixel height of
* every row; `overscan` (default 3) is how many extra rows to render above and
* below the viewport. `parent` must be a scroll container (your CSS: a fixed
* height + `overflow: auto`).
* Keep the rows as a contiguous block that ENDS just before this node, instead
* of at the very end of `parent`. Use it when `parent` also holds non-row
* siblings that must stay put — a trailing "add" button, a sliding indicator:
* `before: () => addButton`. The node (a function is re-read each reconcile, or
* pass the node directly) must be a child of `parent`. Without it, bindList
* assumes exclusive ownership and appends rows to the end. Ignored when
* virtualized (the rows own bindList's inner sizer exclusively).
*/
before?: Node | (() => Node | null);
/**
* Turn on viewport virtualization. `parent` must be a scroll container (your
* CSS: a fixed height + `overflow: auto`). `overscan` (default 3) is how many
* extra rows to render above and below the viewport.
*
* `rowHeight` (a {@link RowHeight}) is the height model:
* - **`number`** — every row is this fixed pixel height. O(1) windowing, no
* cumulative model built.
* - **`(item, index) => number`** — app-declared **variable** heights, derived
* purely from the item and its index. kerf builds a prefix sum of the
* heights (rebuilt when the source array changes, not per scroll frame) and
* binary-searches it to find the visible window. Return a non-negative
* number of pixels.
* - **`{ estimate }`** — **measured** heights for rows whose height is only
* known after layout. kerf sizes an unmeasured row by `estimate` (a number
* or an `(item, index) => number`), and the app reports each row's real
* height via {@link BindListHandle.setHeight} (or the `observeRowHeights`
* helper). kerf anchor-corrects `scrollTop` when an above-viewport row is
* remeasured, so content doesn't jump.
*/
virtualize?: {
rowHeight: number;
rowHeight: RowHeight<T>;
overscan?: number;

@@ -63,4 +113,19 @@ };

*/
declare function bindList<T>(parent: HTMLElement, source: ListSource<T>, options: BindListOptions<T>): () => void;
declare function bindList<T>(parent: HTMLElement, source: ListSource<T>, options: BindListOptions<T>): BindListHandle;
/**
* Drive a **measured** virtualized `bindList` (`virtualize: { rowHeight: {
* estimate } }`) from real layout: install ONE `ResizeObserver` over the visible
* rows and forward each row's `offsetHeight` to `handle.setHeight`, re-observing
* as the window shifts. Returns a disposer.
*
* This is the batteries-included measurement path; it is deliberately separate
* from `bindList` (which never depends on `ResizeObserver`) — you can measure
* however you like and call `handle.setHeight` yourself instead. A no-op for a
* non-virtualized handle or where `ResizeObserver` is unavailable (SSR).
*
* const list = bindList(scrollEl, source, { key, render, virtualize: { rowHeight: { estimate: 64 } } });
* const stopMeasuring = observeRowHeights(list);
*/
declare function observeRowHeights(handle: BindListHandle): () => void;
export { type BindListOptions, type ListKey, type ListSource, type RowElement, bindList };
export { type BindListHandle, type BindListOptions, type ListKey, type ListSource, type RowElement, type RowHeight, bindList, observeRowHeights };
import { ARRAY_SIGNAL_BRAND } from './chunk-MRYM3O3V.js';
import { mount } from './chunk-4MY2656S.js';
import { mount } from './chunk-LKWAKC2X.js';
import './chunk-QIP723L4.js';
import './chunk-YHH7OUFA.js';
import './chunk-FSAQR6IU.js';
import './chunk-SUPUPSBE.js';
import { effect } from './chunk-3APBEVHF.js';

@@ -12,4 +12,8 @@ import './chunk-GY4XV2UV.js';

function bindList(parent, source, options) {
const { key, render, tag = "div", virtualize } = options;
const { key, render, tag = "div", virtualize, before } = options;
const overscan = virtualize?.overscan ?? 3;
const endAnchor = () => {
if (virtualize !== void 0 || before === void 0) return null;
return (typeof before === "function" ? before() : before) ?? null;
};
const rows = /* @__PURE__ */ new Map();

@@ -38,9 +42,7 @@ const order = [];

if (elementRow !== null) {
if (virtualize !== void 0) elementRow.el.style.height = `${virtualize.rowHeight}px`;
return { el: elementRow.el, item, dispose: elementRow.dispose, elementMode: true, update: elementRow.update };
}
const el = document.createElement(tag);
if (virtualize !== void 0) el.style.height = `${virtualize.rowHeight}px`;
const dispose = mount(el, () => render(item));
return { el, item, dispose, elementMode: false };
const dispose2 = mount(el, () => render(item));
return { el, item, dispose: dispose2, elementMode: false };
};

@@ -84,3 +86,3 @@ const reconcileItem = (row, k, item) => {

}
let ref = null;
let ref = endAnchor();
for (let i = order.length - 1; i >= 0; i--) {

@@ -100,3 +102,3 @@ const el = order[i].el;

order.splice(patch.index, 0, row);
container.insertBefore(row.el, order[patch.index + 1]?.el ?? null);
container.insertBefore(row.el, order[patch.index + 1]?.el ?? endAnchor());
} else if (patch.type === "remove") {

@@ -110,3 +112,3 @@ const [row] = order.splice(patch.index, 1);

order.splice(patch.to, 0, row);
container.insertBefore(row.el, order[patch.to + 1]?.el ?? null);
container.insertBefore(row.el, order[patch.to + 1]?.el ?? endAnchor());
} else if (patch.type === "update") {

@@ -131,3 +133,3 @@ const current = order[patch.index];

order[patch.index] = row;
container.insertBefore(row.el, order[patch.index + 1]?.el ?? null);
container.insertBefore(row.el, order[patch.index + 1]?.el ?? endAnchor());
}

@@ -138,2 +140,58 @@ }

};
const rowHeight = virtualize?.rowHeight;
const fixedHeight = typeof rowHeight === "number" ? rowHeight : null;
const measuring = typeof rowHeight === "object" && rowHeight !== null;
const measured = /* @__PURE__ */ new Map();
const estimateAt = (index) => {
const est = rowHeight.estimate;
return typeof est === "function" ? est(items[index], index) : est;
};
const variableHeightAt = fixedHeight !== null ? null : measuring ? (index) => {
const k = key(items[index]);
return measured.has(k) ? measured.get(k) : estimateAt(index);
} : (index) => rowHeight(items[index], index);
let offsets = [0];
let heightsDirty = true;
const indexByKey = /* @__PURE__ */ new Map();
let pendingAnchorDelta = 0;
const rebuildOffsets = () => {
const fn = variableHeightAt;
const total = items.length;
offsets = new Array(total + 1);
offsets[0] = 0;
if (measuring) indexByKey.clear();
for (let i = 0; i < total; i++) {
offsets[i + 1] = offsets[i] + fn(i);
if (measuring) indexByKey.set(key(items[i]), i);
}
};
const findStart = (target, total) => {
let lo = 0;
let hi = total;
while (lo < hi) {
const mid = lo + hi + 1 >> 1;
if (offsets[mid] <= target) lo = mid;
else hi = mid - 1;
}
return lo;
};
const findEnd = (target, total) => {
let lo = 0;
let hi = total;
while (lo < hi) {
const mid = lo + hi >> 1;
if (offsets[mid] >= target) hi = mid;
else lo = mid + 1;
}
return lo;
};
const sizeVisibleRows = (start) => {
if (measuring) return;
for (let j = 0; j < order.length; j++) {
const abs = start + j;
const h = fixedHeight !== null ? fixedHeight : offsets[abs + 1] - offsets[abs];
order[j].el.style.height = `${h}px`;
}
};
const renderSubscribers = /* @__PURE__ */ new Set();
const renderWindow = () => {

@@ -152,15 +210,36 @@ if (virtualize === void 0) {

}
const { rowHeight } = virtualize;
const total = items.length;
const start = Math.max(0, Math.floor(parent.scrollTop / rowHeight) - overscan);
const end = Math.min(total, Math.ceil((parent.scrollTop + parent.clientHeight) / rowHeight) + overscan);
const scrollTop = parent.scrollTop;
const viewportBottom = scrollTop + parent.clientHeight;
let start;
let end;
let padTop;
let padBottom;
if (fixedHeight !== null) {
start = Math.max(0, Math.floor(scrollTop / fixedHeight) - overscan);
end = Math.min(total, Math.ceil(viewportBottom / fixedHeight) + overscan);
padTop = start * fixedHeight;
padBottom = Math.max(0, total - end) * fixedHeight;
} else {
if (heightsDirty) {
rebuildOffsets();
heightsDirty = false;
}
start = Math.max(0, findStart(scrollTop, total) - overscan);
end = Math.min(total, findEnd(viewportBottom, total) + overscan);
padTop = offsets[start];
padBottom = offsets[total] - offsets[end];
}
syncRows(items.slice(start, end));
container.style.paddingTop = `${start * rowHeight}px`;
container.style.paddingBottom = `${Math.max(0, total - end) * rowHeight}px`;
sizeVisibleRows(start);
container.style.paddingTop = `${padTop}px`;
container.style.paddingBottom = `${padBottom}px`;
for (const cb of renderSubscribers) cb();
};
const stopEffect = effect(() => {
items = source.value;
heightsDirty = true;
renderWindow();
});
const onScroll = () => {
const scheduleRender = () => {
if (rafPending) return;

@@ -170,7 +249,23 @@ rafPending = true;

rafPending = false;
if (!disposed) renderWindow();
if (disposed) return;
if (pendingAnchorDelta !== 0) {
parent.scrollTop += pendingAnchorDelta;
pendingAnchorDelta = 0;
}
renderWindow();
});
};
if (virtualize !== void 0) parent.addEventListener("scroll", onScroll);
return () => {
if (virtualize !== void 0) parent.addEventListener("scroll", scheduleRender);
const setHeight = (k, height) => {
if (!measuring) return;
const idx = indexByKey.get(k);
if (idx === void 0) return;
const oldHeight = measured.has(k) ? measured.get(k) : estimateAt(idx);
if (height === oldHeight) return;
measured.set(k, height);
if (offsets[idx + 1] <= parent.scrollTop) pendingAnchorDelta += height - oldHeight;
heightsDirty = true;
scheduleRender();
};
const dispose = (() => {
disposed = true;

@@ -183,11 +278,52 @@ stopEffect();

rows.clear();
renderSubscribers.clear();
if (virtualize !== void 0) {
parent.removeEventListener("scroll", onScroll);
parent.removeEventListener("scroll", scheduleRender);
container.remove();
VIRTUAL_INTERNALS.delete(handle);
}
});
const handle = dispose;
handle.setHeight = setHeight;
if (virtualize !== void 0) {
VIRTUAL_INTERNALS.set(handle, {
visibleRows: () => order.map((row) => ({ key: key(row.item), el: row.el })),
onRender: (cb) => {
renderSubscribers.add(cb);
return () => renderSubscribers.delete(cb);
}
});
}
return handle;
}
var VIRTUAL_INTERNALS = /* @__PURE__ */ new WeakMap();
function observeRowHeights(handle) {
const internals = VIRTUAL_INTERNALS.get(handle);
const RO = globalThis.ResizeObserver;
if (internals === void 0 || RO === void 0) return () => {
};
const keyByEl = /* @__PURE__ */ new WeakMap();
const observer = new RO((entries) => {
for (const entry of entries) {
const k = keyByEl.get(entry.target);
if (k !== void 0) handle.setHeight(k, entry.target.offsetHeight);
}
});
const resync = () => {
observer.disconnect();
for (const { key: k, el } of internals.visibleRows()) {
keyByEl.set(el, k);
observer.observe(el);
}
};
const unsubscribe = internals.onRender(resync);
resync();
return () => {
observer.disconnect();
unsubscribe();
};
}
export { bindList };
export { bindList, observeRowHeights };
//# sourceMappingURL=list.js.map
//# sourceMappingURL=list.js.map

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/list.ts"],"names":[],"mappings":"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","file":"list.js","sourcesContent":["/**\n * `kerfjs/list` — `bindList`, a keyed list with a live per-row mount and\n * optional viewport virtualization.\n *\n * This is a DELIBERATE second list API, distinct from `each()`. It does two\n * things `each()` structurally cannot:\n * 1. **Per-row reactivity.** Every row is individually `mount()`ed, so a signal\n * the row's `render` reads updates just that row (fine-grained binding or a\n * one-row morph) without touching its siblings — no full-list pass.\n * 2. **Virtualization.** With `{ virtualize: { rowHeight } }` only the rows in\n * the scroll viewport are rendered; padding on the scroll container keeps\n * `scrollHeight` honest.\n *\n * `each()` stays the choice for item-owned-state lists rendered to HTML strings;\n * reach for `bindList` when you need surgical per-row updates or windowing.\n *\n * import { bindList } from 'kerfjs/list';\n *\n * const dispose = bindList(listEl, itemsSignal, {\n * key: (row) => row.id,\n * render: (row) => <span class={selected} data-id={row.id}>{row.label}</span>,\n * tag: 'li',\n * virtualize: { rowHeight: 32 },\n * });\n *\n * `render` reads signals for reactivity (external state like a `selectedId`, or\n * signals the item carries) — keep the item OBJECTS stable across renders and\n * drive structure (add/remove/move) through `itemsSignal`. A row whose item\n * object identity changes is rebuilt (same rule as `each()`'s memo). `bindList`\n * OWNS `parent`'s children. It reads `itemsSignal.value`, so a plain\n * `signal<T[]>` or an `arraySignal<T>` both work.\n */\nimport { ARRAY_SIGNAL_BRAND, type ArrayPatch } from './array-signal.js';\nimport { mount, type MountResult } from './mount.js';\nimport { effect } from './reactive.js';\n\n/** A row's stable key. */\nexport type ListKey = string | number;\n\n/** Anything with a tracking `.value` array read — a `signal<readonly T[]>` or an `arraySignal<T>`. */\nexport interface ListSource<T> {\n readonly value: readonly T[];\n}\n\n/**\n * A row built imperatively by `render`: return the row **element** itself (kerf\n * keys/moves/reuses it and owns nothing inside it), or `{ el, update?, dispose? }`\n * to also hand back an `update(item)` — called on the SAME element when the row's\n * key persists but its item changes — and a `dispose` that runs only when the row\n * is removed.\n */\nexport type RowElement<T> =\n | HTMLElement\n | { el: HTMLElement; update?: (item: T) => void; dispose?: () => void };\n\n/** Options for {@link bindList}. */\nexport interface BindListOptions<T> {\n /** Stable per-row key. Rows are matched, moved, and reused by this. */\n key: (item: T) => ListKey;\n /**\n * Build a row. Two modes, chosen per call by what you return:\n * - **Content mode** (a `MountResult` — JSX / `SafeHtml`): kerf creates the\n * row element (`tag`) and `mount()`s your content inside it, so signals your\n * content reads drive per-row reactivity.\n * - **Element mode** (an `HTMLElement`, or `{ el, update?, dispose? }`): the\n * element you return IS the row, so you own its tag, class, `data-*`, and\n * listeners. kerf **keys/moves/reuses** it — the SAME element survives an\n * append/remove/reorder or a fresh item object at the same key. Refresh its\n * content by reading signals inside it, or by returning an `update(item)`\n * that kerf calls on the existing element when the item changes. `dispose`\n * runs only when the row is genuinely removed.\n */\n render: (item: T) => MountResult | RowElement<T>;\n /** Row element tag for **content mode**. Default `'div'` (use `'li'` inside a `<ul>`, `'tr'` inside a `<tbody>`, …). Ignored in element mode. */\n tag?: string;\n /**\n * Turn on viewport virtualization. `rowHeight` is the fixed pixel height of\n * every row; `overscan` (default 3) is how many extra rows to render above and\n * below the viewport. `parent` must be a scroll container (your CSS: a fixed\n * height + `overflow: auto`).\n */\n virtualize?: { rowHeight: number; overscan?: number };\n}\n\ninterface Row<T> {\n el: HTMLElement;\n item: T;\n dispose: () => void;\n /** True for element-mode rows (the caller owns the element — reuse it, don't rebuild on item change). */\n elementMode: boolean;\n /** Element mode only: refresh the existing element when the item changes at the same key. */\n update?: (item: T) => void;\n}\n\n/**\n * Bind a keyed, per-row-reactive list to `parent`, driven by `source` (a\n * `signal<readonly T[]>` or an `arraySignal<T>`). Returns a disposer that tears\n * down every row mount, the scroll listener (if virtualized), and the source\n * subscription.\n */\nexport function bindList<T>(\n parent: HTMLElement,\n source: ListSource<T>,\n options: BindListOptions<T>,\n): () => void {\n const { key, render, tag = 'div', virtualize } = options;\n const overscan = virtualize?.overscan ?? 3;\n\n const rows = new Map<ListKey, Row<T>>();\n // The current DOM order of rows, kept in step by both the keyed-diff and the\n // granular patch paths so index-based patches can address rows directly.\n const order: Array<Row<T>> = [];\n let items: readonly T[] = [];\n let disposed = false;\n let rafPending = false;\n let firstRender = true;\n\n // Granular fast path (KF-478): when the source is an `arraySignal` and the\n // list is NOT virtualized, apply its insert/remove/move/update patches\n // directly in O(patches) instead of diffing the whole snapshot. Virtualized\n // lists keep the keyed diff — their visible set is just the window (cheap),\n // and absolute-index patches don't compose with a shifting window. A plain\n // `signal<T[]>` has no patches, so it always uses the keyed diff.\n const patchSource = source as {\n [ARRAY_SIGNAL_BRAND]?: boolean;\n _consumePatches?: () => ArrayPatch<T>[];\n };\n const granularEligible = virtualize === undefined && patchSource[ARRAY_SIGNAL_BRAND] === true;\n\n // Virtualized lists put the windowing padding + rows on an INNER sizer, so the\n // padding never inflates the scroll container's clientHeight (padding counts\n // toward clientHeight). `parent` stays the clean scroll viewport; `container`\n // holds the rows. Non-virtualized lists render straight into `parent`.\n const container: HTMLElement = virtualize === undefined ? parent : document.createElement('div');\n if (virtualize !== undefined) parent.appendChild(container);\n\n const NOOP = (): void => { /* element-mode rows with no caller teardown */ };\n\n // Detect element mode from a render result: a raw `HTMLElement`, or a\n // `{ el, dispose? }` object. Everything else (SafeHtml / string / nullish) is\n // content mode. SafeHtml is an object but has no `el`, so it never matches.\n const asElementRow = (\n rendered: MountResult | RowElement<T>,\n ): { el: HTMLElement; dispose: () => void; update?: (item: T) => void } | null => {\n if (rendered instanceof HTMLElement) return { el: rendered, dispose: NOOP };\n if (\n rendered !== null\n && typeof rendered === 'object'\n && 'el' in rendered\n && (rendered as { el: unknown }).el instanceof HTMLElement\n ) {\n const r = rendered as { el: HTMLElement; update?: (item: T) => void; dispose?: () => void };\n return { el: r.el, dispose: r.dispose ?? NOOP, update: r.update };\n }\n return null;\n };\n\n const makeRow = (item: T): Row<T> => {\n // One call decides the mode per row (so a list may mix element + content rows).\n const elementRow = asElementRow(render(item));\n if (elementRow !== null) {\n // Element mode: the returned element IS the row; the caller owns its\n // content + cleanup. bindList still sizes it for the windowing math.\n if (virtualize !== undefined) elementRow.el.style.height = `${virtualize.rowHeight}px`;\n return { el: elementRow.el, item, dispose: elementRow.dispose, elementMode: true, update: elementRow.update };\n }\n // Content mode: kerf creates the row element and mounts `render` inside it,\n // so the content is per-row reactive. (In content mode `render` runs once\n // more here for the mode probe than the mount itself needs — keep it a pure\n // projection, which bindList already requires.)\n const el = document.createElement(tag);\n if (virtualize !== undefined) el.style.height = `${virtualize.rowHeight}px`;\n // Content mode: `render` returns a MountResult here (element results were\n // handled above), so narrowing it for `mount` is sound.\n const dispose = mount(el, () => render(item) as MountResult);\n return { el, item, dispose, elementMode: false };\n };\n\n // A row whose KEY persists but whose item object changed. Content-mode rows are\n // rebuilt (their mount re-renders the fresh item); element-mode rows are REUSED\n // — the caller owns the element, so we keep it (preserving focus / scroll /\n // listeners) and refresh via the optional `update(item)`. Returns the row to\n // use at that key (a fresh one for content, the same one for element).\n const reconcileItem = (row: Row<T>, k: ListKey, item: T): Row<T> => {\n if (row.item === item) return row;\n if (row.elementMode) {\n row.item = item;\n row.update?.(item);\n return row;\n }\n row.dispose();\n row.el.remove();\n rows.delete(k);\n const fresh = makeRow(item);\n rows.set(k, fresh);\n return fresh;\n };\n\n // Reconcile the live rows to exactly `visible`, in order, keyed.\n const syncRows = (visible: readonly T[]): void => {\n const wanted = new Set<ListKey>();\n for (const item of visible) wanted.add(key(item));\n\n // Remove rows that are gone from the window.\n for (const [k, row] of rows) {\n if (!wanted.has(k)) {\n row.dispose();\n row.el.remove();\n rows.delete(k);\n }\n }\n\n // Create missing rows; reuse existing ones by key (element rows keep their\n // element across item changes; content rows rebuild on identity change).\n order.length = 0;\n for (const item of visible) {\n const k = key(item);\n const existing = rows.get(k);\n let row: Row<T>;\n if (existing !== undefined) {\n row = reconcileItem(existing, k, item);\n } else {\n row = makeRow(item);\n rows.set(k, row);\n }\n order.push(row);\n }\n\n // Reverse pass: move only rows that are out of position.\n let ref: Node | null = null;\n for (let i = order.length - 1; i >= 0; i--) {\n const el = order[i].el;\n if (el.parentNode !== container || el.nextSibling !== ref) {\n container.insertBefore(el, ref);\n }\n ref = el;\n }\n };\n\n // Apply arraySignal structural patches directly to `order` + the DOM, in\n // O(patches). Indices are always valid by construction: `order` reflects the\n // last-rendered state and the patches are exactly the delta from it (bindList\n // drains the queue every render, and `replace` is filtered out by the caller,\n // which snapshots instead). The `splice()`s mirror `arraySignal`'s own\n // `_items` mutations exactly.\n const applyPatches = (patches: readonly ArrayPatch<T>[]): void => {\n for (const patch of patches) {\n if (patch.type === 'insert') {\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order.splice(patch.index, 0, row);\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? null);\n } else if (patch.type === 'remove') {\n const [row] = order.splice(patch.index, 1);\n row.dispose();\n row.el.remove();\n rows.delete(key(row.item));\n } else if (patch.type === 'move') {\n const [row] = order.splice(patch.from, 1);\n order.splice(patch.to, 0, row);\n container.insertBefore(row.el, order[patch.to + 1]?.el ?? null);\n } else if (patch.type === 'update') {\n // An item whose OBJECT identity changed: content rows rebuild (their mount\n // re-renders the fresh item); element rows are REUSED — keep the caller's\n // element and refresh via update(), re-keying if the key changed. A\n // same-ref update needs nothing (the row's mount reacts to its signals).\n const current = order[patch.index];\n if (current.item !== patch.item) {\n if (current.elementMode) {\n const oldKey = key(current.item);\n const newKey = key(patch.item);\n current.item = patch.item;\n if (newKey !== oldKey) {\n rows.delete(oldKey);\n rows.set(newKey, current);\n }\n current.update?.(patch.item);\n } else {\n current.dispose();\n current.el.remove();\n rows.delete(key(current.item));\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order[patch.index] = row;\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? null);\n }\n }\n }\n // 'replace' never reaches here — the caller snapshots on it.\n }\n };\n\n const renderWindow = (): void => {\n if (virtualize === undefined) {\n if (granularEligible) {\n // Always drain to keep the single patch queue clean (so patches never\n // double-apply). Take the granular path past the first render, when\n // there are patches, and none is a `replace` (which reshapes the whole\n // array — snapshot instead). Otherwise fall through to a keyed diff.\n const patches = patchSource._consumePatches!();\n if (\n !firstRender\n && patches.length > 0\n && !patches.some((p) => p.type === 'replace')\n ) {\n applyPatches(patches);\n return;\n }\n }\n syncRows(items);\n firstRender = false;\n return;\n }\n const { rowHeight } = virtualize;\n const total = items.length;\n const start = Math.max(0, Math.floor(parent.scrollTop / rowHeight) - overscan);\n const end = Math.min(total, Math.ceil((parent.scrollTop + parent.clientHeight) / rowHeight) + overscan);\n syncRows(items.slice(start, end));\n container.style.paddingTop = `${start * rowHeight}px`;\n container.style.paddingBottom = `${Math.max(0, total - end) * rowHeight}px`;\n };\n\n const stopEffect = effect(() => {\n items = source.value; // tracking read — re-runs on any structural change\n renderWindow();\n });\n\n const onScroll = (): void => {\n if (rafPending) return;\n rafPending = true;\n globalThis.requestAnimationFrame(() => {\n rafPending = false;\n if (!disposed) renderWindow();\n });\n };\n if (virtualize !== undefined) parent.addEventListener('scroll', onScroll);\n\n return () => {\n disposed = true;\n stopEffect();\n for (const row of rows.values()) {\n row.dispose();\n if (virtualize === undefined) row.el.remove();\n }\n rows.clear();\n if (virtualize !== undefined) {\n parent.removeEventListener('scroll', onScroll);\n container.remove(); // removes the inner sizer and its rows in one go\n }\n };\n}\n"]}
{"version":3,"sources":["../src/list.ts"],"names":["dispose"],"mappings":"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","file":"list.js","sourcesContent":["/**\n * `kerfjs/list` — `bindList`, a keyed list with a live per-row mount and\n * optional viewport virtualization.\n *\n * This is a DELIBERATE second list API, distinct from `each()`. It does two\n * things `each()` structurally cannot:\n * 1. **Per-row reactivity.** Every row is individually `mount()`ed, so a signal\n * the row's `render` reads updates just that row (fine-grained binding or a\n * one-row morph) without touching its siblings — no full-list pass.\n * 2. **Virtualization.** With `{ virtualize: { rowHeight } }` only the rows in\n * the scroll viewport are rendered; padding on the scroll container keeps\n * `scrollHeight` honest. `rowHeight` is a fixed `number` (O(1) windowing), a\n * `(item, index) => number` for **app-declared variable** heights (a prefix\n * sum + binary-search window), or `{ estimate }` for **measured** heights —\n * the app reports real heights via the returned handle's `setHeight` (or the\n * `observeRowHeights` helper) and kerf anchor-corrects `scrollTop`. See\n * `docs/17-list-virtualization.md`.\n *\n * `each()` stays the choice for item-owned-state lists rendered to HTML strings;\n * reach for `bindList` when you need surgical per-row updates or windowing.\n *\n * import { bindList } from 'kerfjs/list';\n *\n * const dispose = bindList(listEl, itemsSignal, {\n * key: (row) => row.id,\n * render: (row) => <span class={selected} data-id={row.id}>{row.label}</span>,\n * tag: 'li',\n * virtualize: { rowHeight: 32 },\n * });\n *\n * `render` reads signals for reactivity (external state like a `selectedId`, or\n * signals the item carries) — keep the item OBJECTS stable across renders and\n * drive structure (add/remove/move) through `itemsSignal`. A row whose item\n * object identity changes is rebuilt (same rule as `each()`'s memo). `bindList`\n * OWNS `parent`'s children by default (append/move to the end) — to share\n * `parent` with fixed trailing siblings (an \"add\" button, an indicator), pass\n * `before` so the rows end just before that node. It reads `itemsSignal.value`,\n * so a plain `signal<T[]>` or an `arraySignal<T>` both work.\n */\nimport { ARRAY_SIGNAL_BRAND, type ArrayPatch } from './array-signal.js';\nimport { mount, type MountResult } from './mount.js';\nimport { effect } from './reactive.js';\n\n/** A row's stable key. */\nexport type ListKey = string | number;\n\n/** Anything with a tracking `.value` array read — a `signal<readonly T[]>` or an `arraySignal<T>`. */\nexport interface ListSource<T> {\n readonly value: readonly T[];\n}\n\n/**\n * A row built imperatively by `render`: return the row **element** itself (kerf\n * keys/moves/reuses it and owns nothing inside it), or `{ el, update?, dispose? }`\n * to also hand back an `update(item)` — called on the SAME element when the row's\n * key persists but its item changes — and a `dispose` that runs only when the row\n * is removed.\n */\nexport type RowElement<T> =\n | HTMLElement\n | { el: HTMLElement; update?: (item: T) => void; dispose?: () => void };\n\n/**\n * The virtualization height model:\n * - **`number`** — every row is this fixed pixel height (O(1) windowing).\n * - **`(item, index) => number`** — app-declared **variable** heights, derived\n * purely from the item and its index.\n * - **`{ estimate }`** — **measured** heights: kerf uses `estimate` for a row\n * until the app reports its real height through {@link BindListHandle.setHeight}\n * (or the `observeRowHeights` helper). See `docs/17-list-virtualization.md`.\n */\nexport type RowHeight<T> =\n | number\n | ((item: T, index: number) => number)\n | { estimate: number | ((item: T, index: number) => number) };\n\n/**\n * The value {@link bindList} returns: a disposer you call to tear the list down,\n * augmented with `setHeight` for the **measured** virtualization mode.\n */\nexport type BindListHandle = (() => void) & {\n /**\n * Report a row's real pixel height (measured after layout) for\n * `virtualize: { rowHeight: { estimate } }` lists. Keyed by the list `key`, so\n * a report survives reorders. kerf recomputes the window and, if the row sits\n * ABOVE the viewport, anchor-corrects `scrollTop` so content doesn't jump.\n * A no-op for fixed / declared-height lists and for unknown keys.\n */\n setHeight: (key: ListKey, height: number) => void;\n};\n\n/** Options for {@link bindList}. */\nexport interface BindListOptions<T> {\n /** Stable per-row key. Rows are matched, moved, and reused by this. */\n key: (item: T) => ListKey;\n /**\n * Build a row. Two modes, chosen per call by what you return:\n * - **Content mode** (a `MountResult` — JSX / `SafeHtml`): kerf creates the\n * row element (`tag`) and `mount()`s your content inside it, so signals your\n * content reads drive per-row reactivity.\n * - **Element mode** (an `HTMLElement`, or `{ el, update?, dispose? }`): the\n * element you return IS the row, so you own its tag, class, `data-*`, and\n * listeners. kerf **keys/moves/reuses** it — the SAME element survives an\n * append/remove/reorder or a fresh item object at the same key. Refresh its\n * content by reading signals inside it, or by returning an `update(item)`\n * that kerf calls on the existing element when the item changes. `dispose`\n * runs only when the row is genuinely removed.\n */\n render: (item: T) => MountResult | RowElement<T>;\n /** Row element tag for **content mode**. Default `'div'` (use `'li'` inside a `<ul>`, `'tr'` inside a `<tbody>`, …). Ignored in element mode. */\n tag?: string;\n /**\n * Keep the rows as a contiguous block that ENDS just before this node, instead\n * of at the very end of `parent`. Use it when `parent` also holds non-row\n * siblings that must stay put — a trailing \"add\" button, a sliding indicator:\n * `before: () => addButton`. The node (a function is re-read each reconcile, or\n * pass the node directly) must be a child of `parent`. Without it, bindList\n * assumes exclusive ownership and appends rows to the end. Ignored when\n * virtualized (the rows own bindList's inner sizer exclusively).\n */\n before?: Node | (() => Node | null);\n /**\n * Turn on viewport virtualization. `parent` must be a scroll container (your\n * CSS: a fixed height + `overflow: auto`). `overscan` (default 3) is how many\n * extra rows to render above and below the viewport.\n *\n * `rowHeight` (a {@link RowHeight}) is the height model:\n * - **`number`** — every row is this fixed pixel height. O(1) windowing, no\n * cumulative model built.\n * - **`(item, index) => number`** — app-declared **variable** heights, derived\n * purely from the item and its index. kerf builds a prefix sum of the\n * heights (rebuilt when the source array changes, not per scroll frame) and\n * binary-searches it to find the visible window. Return a non-negative\n * number of pixels.\n * - **`{ estimate }`** — **measured** heights for rows whose height is only\n * known after layout. kerf sizes an unmeasured row by `estimate` (a number\n * or an `(item, index) => number`), and the app reports each row's real\n * height via {@link BindListHandle.setHeight} (or the `observeRowHeights`\n * helper). kerf anchor-corrects `scrollTop` when an above-viewport row is\n * remeasured, so content doesn't jump.\n */\n virtualize?: { rowHeight: RowHeight<T>; overscan?: number };\n}\n\ninterface Row<T> {\n el: HTMLElement;\n item: T;\n dispose: () => void;\n /** True for element-mode rows (the caller owns the element — reuse it, don't rebuild on item change). */\n elementMode: boolean;\n /** Element mode only: refresh the existing element when the item changes at the same key. */\n update?: (item: T) => void;\n}\n\n/**\n * Bind a keyed, per-row-reactive list to `parent`, driven by `source` (a\n * `signal<readonly T[]>` or an `arraySignal<T>`). Returns a disposer that tears\n * down every row mount, the scroll listener (if virtualized), and the source\n * subscription.\n */\nexport function bindList<T>(\n parent: HTMLElement,\n source: ListSource<T>,\n options: BindListOptions<T>,\n): BindListHandle {\n const { key, render, tag = 'div', virtualize, before } = options;\n const overscan = virtualize?.overscan ?? 3;\n\n // The node the row block ends before — `before` (KF-496) when the list shares\n // `parent` with trailing siblings, else the end of the container. Never applies\n // when virtualized: the rows own bindList's inner sizer exclusively.\n const endAnchor = (): Node | null => {\n if (virtualize !== undefined || before === undefined) return null;\n return (typeof before === 'function' ? before() : before) ?? null;\n };\n\n const rows = new Map<ListKey, Row<T>>();\n // The current DOM order of rows, kept in step by both the keyed-diff and the\n // granular patch paths so index-based patches can address rows directly.\n const order: Array<Row<T>> = [];\n let items: readonly T[] = [];\n let disposed = false;\n let rafPending = false;\n let firstRender = true;\n\n // Granular fast path (KF-478): when the source is an `arraySignal` and the\n // list is NOT virtualized, apply its insert/remove/move/update patches\n // directly in O(patches) instead of diffing the whole snapshot. Virtualized\n // lists keep the keyed diff — their visible set is just the window (cheap),\n // and absolute-index patches don't compose with a shifting window. A plain\n // `signal<T[]>` has no patches, so it always uses the keyed diff.\n const patchSource = source as {\n [ARRAY_SIGNAL_BRAND]?: boolean;\n _consumePatches?: () => ArrayPatch<T>[];\n };\n const granularEligible = virtualize === undefined && patchSource[ARRAY_SIGNAL_BRAND] === true;\n\n // Virtualized lists put the windowing padding + rows on an INNER sizer, so the\n // padding never inflates the scroll container's clientHeight (padding counts\n // toward clientHeight). `parent` stays the clean scroll viewport; `container`\n // holds the rows. Non-virtualized lists render straight into `parent`.\n const container: HTMLElement = virtualize === undefined ? parent : document.createElement('div');\n if (virtualize !== undefined) parent.appendChild(container);\n\n const NOOP = (): void => { /* element-mode rows with no caller teardown */ };\n\n // Detect element mode from a render result: a raw `HTMLElement`, or a\n // `{ el, dispose? }` object. Everything else (SafeHtml / string / nullish) is\n // content mode. SafeHtml is an object but has no `el`, so it never matches.\n const asElementRow = (\n rendered: MountResult | RowElement<T>,\n ): { el: HTMLElement; dispose: () => void; update?: (item: T) => void } | null => {\n if (rendered instanceof HTMLElement) return { el: rendered, dispose: NOOP };\n if (\n rendered !== null\n && typeof rendered === 'object'\n && 'el' in rendered\n && (rendered as { el: unknown }).el instanceof HTMLElement\n ) {\n const r = rendered as { el: HTMLElement; update?: (item: T) => void; dispose?: () => void };\n return { el: r.el, dispose: r.dispose ?? NOOP, update: r.update };\n }\n return null;\n };\n\n const makeRow = (item: T): Row<T> => {\n // One call decides the mode per row (so a list may mix element + content rows).\n const elementRow = asElementRow(render(item));\n if (elementRow !== null) {\n // Element mode: the returned element IS the row; the caller owns its\n // content + cleanup. bindList sizes it for the windowing math per render\n // (see `sizeVisibleRows`), not here, since a variable height depends on the\n // row's current index in the full list.\n return { el: elementRow.el, item, dispose: elementRow.dispose, elementMode: true, update: elementRow.update };\n }\n // Content mode: kerf creates the row element and mounts `render` inside it,\n // so the content is per-row reactive. (In content mode `render` runs once\n // more here for the mode probe than the mount itself needs — keep it a pure\n // projection, which bindList already requires.)\n const el = document.createElement(tag);\n // Content mode: `render` returns a MountResult here (element results were\n // handled above), so narrowing it for `mount` is sound.\n const dispose = mount(el, () => render(item) as MountResult);\n return { el, item, dispose, elementMode: false };\n };\n\n // A row whose KEY persists but whose item object changed. Content-mode rows are\n // rebuilt (their mount re-renders the fresh item); element-mode rows are REUSED\n // — the caller owns the element, so we keep it (preserving focus / scroll /\n // listeners) and refresh via the optional `update(item)`. Returns the row to\n // use at that key (a fresh one for content, the same one for element).\n const reconcileItem = (row: Row<T>, k: ListKey, item: T): Row<T> => {\n if (row.item === item) return row;\n if (row.elementMode) {\n row.item = item;\n row.update?.(item);\n return row;\n }\n row.dispose();\n row.el.remove();\n rows.delete(k);\n const fresh = makeRow(item);\n rows.set(k, fresh);\n return fresh;\n };\n\n // Reconcile the live rows to exactly `visible`, in order, keyed.\n const syncRows = (visible: readonly T[]): void => {\n const wanted = new Set<ListKey>();\n for (const item of visible) wanted.add(key(item));\n\n // Remove rows that are gone from the window.\n for (const [k, row] of rows) {\n if (!wanted.has(k)) {\n row.dispose();\n row.el.remove();\n rows.delete(k);\n }\n }\n\n // Create missing rows; reuse existing ones by key (element rows keep their\n // element across item changes; content rows rebuild on identity change).\n order.length = 0;\n for (const item of visible) {\n const k = key(item);\n const existing = rows.get(k);\n let row: Row<T>;\n if (existing !== undefined) {\n row = reconcileItem(existing, k, item);\n } else {\n row = makeRow(item);\n rows.set(k, row);\n }\n order.push(row);\n }\n\n // Reverse pass: move only rows that are out of position.\n let ref: Node | null = endAnchor();\n for (let i = order.length - 1; i >= 0; i--) {\n const el = order[i].el;\n if (el.parentNode !== container || el.nextSibling !== ref) {\n container.insertBefore(el, ref);\n }\n ref = el;\n }\n };\n\n // Apply arraySignal structural patches directly to `order` + the DOM, in\n // O(patches). Indices are always valid by construction: `order` reflects the\n // last-rendered state and the patches are exactly the delta from it (bindList\n // drains the queue every render, and `replace` is filtered out by the caller,\n // which snapshots instead). The `splice()`s mirror `arraySignal`'s own\n // `_items` mutations exactly.\n const applyPatches = (patches: readonly ArrayPatch<T>[]): void => {\n for (const patch of patches) {\n if (patch.type === 'insert') {\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order.splice(patch.index, 0, row);\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? endAnchor());\n } else if (patch.type === 'remove') {\n const [row] = order.splice(patch.index, 1);\n row.dispose();\n row.el.remove();\n rows.delete(key(row.item));\n } else if (patch.type === 'move') {\n const [row] = order.splice(patch.from, 1);\n order.splice(patch.to, 0, row);\n container.insertBefore(row.el, order[patch.to + 1]?.el ?? endAnchor());\n } else if (patch.type === 'update') {\n // An item whose OBJECT identity changed: content rows rebuild (their mount\n // re-renders the fresh item); element rows are REUSED — keep the caller's\n // element and refresh via update(), re-keying if the key changed. A\n // same-ref update needs nothing (the row's mount reacts to its signals).\n const current = order[patch.index];\n if (current.item !== patch.item) {\n if (current.elementMode) {\n const oldKey = key(current.item);\n const newKey = key(patch.item);\n current.item = patch.item;\n if (newKey !== oldKey) {\n rows.delete(oldKey);\n rows.set(newKey, current);\n }\n current.update?.(patch.item);\n } else {\n current.dispose();\n current.el.remove();\n rows.delete(key(current.item));\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order[patch.index] = row;\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? endAnchor());\n }\n }\n }\n // 'replace' never reaches here — the caller snapshots on it.\n }\n };\n\n // Virtualization height model, three modes:\n // - `fixedHeight` (a `number`): the O(1) fast path — no cumulative model.\n // - `variableHeightAt` (a function): app-declared per-row heights.\n // - measuring (`{ estimate }`): `variableHeightAt` returns the measured height\n // when the app has reported one (via `setHeight`), else the estimate.\n // In the two variable cases, `offsets[i]` is the total height of rows 0..i-1\n // (a prefix sum, length total+1), so `offsets[i+1] - offsets[i]` is row i's\n // height and `offsets[total]` is the full scroll height. It is rebuilt only\n // when `items` changes or a height is reported (heightsDirty), never per scroll\n // frame — a scroll reuses the prefix sum and pays only the O(log n) searches.\n const rowHeight = virtualize?.rowHeight;\n const fixedHeight = typeof rowHeight === 'number' ? rowHeight : null;\n const measuring = typeof rowHeight === 'object' && rowHeight !== null;\n const measured = new Map<ListKey, number>(); // key → real reported height\n const estimateAt = (index: number): number => {\n const est = (rowHeight as { estimate: number | ((item: T, index: number) => number) }).estimate;\n return typeof est === 'function' ? est(items[index], index) : est;\n };\n const variableHeightAt: ((index: number) => number) | null =\n fixedHeight !== null\n ? null\n : measuring\n ? (index): number => {\n const k = key(items[index]);\n return measured.has(k) ? (measured.get(k) as number) : estimateAt(index);\n }\n : (index): number => (rowHeight as (item: T, index: number) => number)(items[index], index);\n\n let offsets: number[] = [0];\n let heightsDirty = true;\n // Measuring only: key → current absolute index, so `setHeight(key, …)` locates\n // the row in O(1). Rebuilt with the prefix sum when `items` changes.\n const indexByKey = new Map<ListKey, number>();\n // Accumulated scroll-anchor correction: the summed height delta of remeasured\n // rows that sit entirely ABOVE the viewport top, applied to `scrollTop` before\n // the next window render so on-screen content does not jump.\n let pendingAnchorDelta = 0;\n\n const rebuildOffsets = (): void => {\n const fn = variableHeightAt as (index: number) => number;\n const total = items.length;\n offsets = new Array<number>(total + 1);\n offsets[0] = 0;\n if (measuring) indexByKey.clear();\n for (let i = 0; i < total; i++) {\n offsets[i + 1] = offsets[i] + fn(i);\n if (measuring) indexByKey.set(key(items[i]), i);\n }\n };\n\n // Greatest index i in [0, total] with `offsets[i] <= target` — the first row\n // whose top is at or above `target` (the viewport top).\n const findStart = (target: number, total: number): number => {\n let lo = 0;\n let hi = total;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (offsets[mid] <= target) lo = mid;\n else hi = mid - 1;\n }\n return lo;\n };\n\n // Smallest index i in [0, total] with `offsets[i] >= target` — one past the\n // last row that starts before `target` (the viewport bottom). `total` if none.\n const findEnd = (target: number, total: number): number => {\n let lo = 0;\n let hi = total;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (offsets[mid] >= target) hi = mid;\n else lo = mid + 1;\n }\n return lo;\n };\n\n // Size each visible row for the windowing math. `order` holds the visible rows\n // in order, so `order[j]` is the item at absolute index `start + j`.\n // MEASURED mode is the exception: the row must take its NATURAL height so the\n // app (or `observeRowHeights`) can read the real `offsetHeight` — forcing a\n // height here would make the measurement echo the estimate. Its offsets come\n // from `setHeight` reports instead.\n const sizeVisibleRows = (start: number): void => {\n if (measuring) return;\n for (let j = 0; j < order.length; j++) {\n const abs = start + j;\n const h = fixedHeight !== null ? fixedHeight : offsets[abs + 1] - offsets[abs];\n order[j].el.style.height = `${h}px`;\n }\n };\n\n // Called after each virtualized window render (used by `observeRowHeights` to\n // re-observe the current visible rows).\n const renderSubscribers = new Set<() => void>();\n\n const renderWindow = (): void => {\n if (virtualize === undefined) {\n if (granularEligible) {\n // Always drain to keep the single patch queue clean (so patches never\n // double-apply). Take the granular path past the first render, when\n // there are patches, and none is a `replace` (which reshapes the whole\n // array — snapshot instead). Otherwise fall through to a keyed diff.\n const patches = patchSource._consumePatches!();\n if (\n !firstRender\n && patches.length > 0\n && !patches.some((p) => p.type === 'replace')\n ) {\n applyPatches(patches);\n return;\n }\n }\n syncRows(items);\n firstRender = false;\n return;\n }\n const total = items.length;\n const scrollTop = parent.scrollTop;\n const viewportBottom = scrollTop + parent.clientHeight;\n let start: number;\n let end: number;\n let padTop: number;\n let padBottom: number;\n if (fixedHeight !== null) {\n start = Math.max(0, Math.floor(scrollTop / fixedHeight) - overscan);\n end = Math.min(total, Math.ceil(viewportBottom / fixedHeight) + overscan);\n padTop = start * fixedHeight;\n padBottom = Math.max(0, total - end) * fixedHeight;\n } else {\n if (heightsDirty) {\n rebuildOffsets();\n heightsDirty = false;\n }\n start = Math.max(0, findStart(scrollTop, total) - overscan);\n end = Math.min(total, findEnd(viewportBottom, total) + overscan);\n padTop = offsets[start];\n padBottom = offsets[total] - offsets[end];\n }\n syncRows(items.slice(start, end));\n sizeVisibleRows(start);\n container.style.paddingTop = `${padTop}px`;\n container.style.paddingBottom = `${padBottom}px`;\n for (const cb of renderSubscribers) cb();\n };\n\n const stopEffect = effect(() => {\n items = source.value; // tracking read — re-runs on any structural change\n heightsDirty = true; // items changed → the prefix sum (if any) is stale\n renderWindow();\n });\n\n // One rAF-coalesced render, shared by scroll and by measurement reports. A\n // pending anchor correction is applied to `scrollTop` first (which itself may\n // fire a scroll, but with the delta already cleared the follow-up is a no-op).\n const scheduleRender = (): void => {\n if (rafPending) return;\n rafPending = true;\n globalThis.requestAnimationFrame(() => {\n rafPending = false;\n if (disposed) return;\n if (pendingAnchorDelta !== 0) {\n parent.scrollTop += pendingAnchorDelta;\n pendingAnchorDelta = 0;\n }\n renderWindow();\n });\n };\n if (virtualize !== undefined) parent.addEventListener('scroll', scheduleRender);\n\n // Measured mode: report a row's real height. No-op for fixed / declared lists\n // and for keys not currently in the list.\n const setHeight = (k: ListKey, height: number): void => {\n if (!measuring) return;\n const idx = indexByKey.get(k);\n if (idx === undefined) return;\n const oldHeight = measured.has(k) ? (measured.get(k) as number) : estimateAt(idx);\n if (height === oldHeight) return;\n measured.set(k, height);\n // A row whose bottom is at/above the viewport top shifts everything below it\n // (the on-screen content) by the height delta — correct `scrollTop` to match.\n // Uses the CURRENT (pre-rebuild) offsets, which reflect the on-screen layout.\n if (offsets[idx + 1] <= parent.scrollTop) pendingAnchorDelta += height - oldHeight;\n heightsDirty = true;\n scheduleRender();\n };\n\n const dispose = ((): void => {\n disposed = true;\n stopEffect();\n for (const row of rows.values()) {\n row.dispose();\n if (virtualize === undefined) row.el.remove();\n }\n rows.clear();\n renderSubscribers.clear();\n if (virtualize !== undefined) {\n parent.removeEventListener('scroll', scheduleRender);\n container.remove(); // removes the inner sizer and its rows in one go\n VIRTUAL_INTERNALS.delete(handle);\n }\n }) as BindListHandle;\n const handle = dispose;\n handle.setHeight = setHeight;\n\n // Register the coordination surface the `observeRowHeights` helper needs, kept\n // off the public type (a GC-tied WeakMap, so it doesn't count against Design\n // rule 5). Only virtualized lists have a window to observe.\n if (virtualize !== undefined) {\n VIRTUAL_INTERNALS.set(handle, {\n visibleRows: () => order.map((row) => ({ key: key(row.item), el: row.el })),\n onRender: (cb) => {\n renderSubscribers.add(cb);\n return () => renderSubscribers.delete(cb);\n },\n });\n }\n\n return handle;\n}\n\n/** Internal coordination surface between {@link bindList} and {@link observeRowHeights}. */\ninterface VirtualInternals {\n /** The current visible rows, in order, with their keys. */\n visibleRows: () => Array<{ key: ListKey; el: HTMLElement }>;\n /** Subscribe to each window render; returns an unsubscribe. */\n onRender: (cb: () => void) => () => void;\n}\n\n// GC-tied (WeakMap) coordination store — a pure cache, not counted against\n// Design rule 5 (same class as `bindings.ts:insertedTextNodes`).\nconst VIRTUAL_INTERNALS = new WeakMap<object, VirtualInternals>();\n\n/**\n * Drive a **measured** virtualized `bindList` (`virtualize: { rowHeight: {\n * estimate } }`) from real layout: install ONE `ResizeObserver` over the visible\n * rows and forward each row's `offsetHeight` to `handle.setHeight`, re-observing\n * as the window shifts. Returns a disposer.\n *\n * This is the batteries-included measurement path; it is deliberately separate\n * from `bindList` (which never depends on `ResizeObserver`) — you can measure\n * however you like and call `handle.setHeight` yourself instead. A no-op for a\n * non-virtualized handle or where `ResizeObserver` is unavailable (SSR).\n *\n * const list = bindList(scrollEl, source, { key, render, virtualize: { rowHeight: { estimate: 64 } } });\n * const stopMeasuring = observeRowHeights(list);\n */\nexport function observeRowHeights(handle: BindListHandle): () => void {\n const internals = VIRTUAL_INTERNALS.get(handle);\n const RO = globalThis.ResizeObserver;\n if (internals === undefined || RO === undefined) return () => { /* nothing to observe */ };\n\n const keyByEl = new WeakMap<Element, ListKey>();\n const observer = new RO((entries) => {\n for (const entry of entries) {\n const k = keyByEl.get(entry.target);\n if (k !== undefined) handle.setHeight(k, (entry.target as HTMLElement).offsetHeight);\n }\n });\n\n const resync = (): void => {\n observer.disconnect();\n for (const { key: k, el } of internals.visibleRows()) {\n keyByEl.set(el, k);\n observer.observe(el);\n }\n };\n\n const unsubscribe = internals.onRender(resync);\n resync(); // observe the initial window\n\n return () => {\n observer.disconnect();\n unsubscribe();\n };\n}\n"]}

@@ -383,4 +383,11 @@ import { SafeHtml } from './jsx-runtime.js';

el: HTMLElement;
/** Dismiss it early (running the `exitClass` transition if set). Idempotent. */
dismiss(): void;
/**
* Dismiss it early. Default runs the `exitClass` transition (removed after
* `exitDuration`); pass `{ instant: true }` to remove it **synchronously** with
* no exit — for an action button that immediately shows a replacement toast in a
* single centered slot (no cross-fade). Idempotent.
*/
dismiss(options?: {
instant?: boolean;
}): void;
}

@@ -387,0 +394,0 @@ /**

@@ -1,6 +0,6 @@

import { mount } from './chunk-4MY2656S.js';
import { mount } from './chunk-LKWAKC2X.js';
import { delegate } from './chunk-KEZTD6H4.js';
import './chunk-QIP723L4.js';
import './chunk-YHH7OUFA.js';
import { jsx } from './chunk-FSAQR6IU.js';
import { jsx } from './chunk-SUPUPSBE.js';
import './chunk-3APBEVHF.js';

@@ -537,3 +537,3 @@ import './chunk-GY4XV2UV.js';

const disposeMount = mount(el, typeof content === "function" ? content : () => content);
const state = { dismissed: false, timer: void 0 };
const state = { dismissed: false, removed: false, timer: void 0, exitTimer: void 0 };
if (enterClass !== void 0) {

@@ -545,2 +545,6 @@ globalThis.requestAnimationFrame(() => {

const remove = () => {
if (state.removed) return;
state.removed = true;
state.dismissed = true;
if (state.exitTimer !== void 0) clearTimeout(state.exitTimer);
disposeMount();

@@ -550,14 +554,14 @@ el.remove();

};
const finish = (instant) => {
const removeNow = () => {
if (state.timer !== void 0) clearTimeout(state.timer);
remove();
};
const fadeOut = () => {
if (state.dismissed) return;
state.dismissed = true;
if (state.timer !== void 0) clearTimeout(state.timer);
if (instant) {
remove();
return;
}
if (enterClass !== void 0) el.classList.remove(enterClass);
if (exitClass !== void 0) el.classList.add(exitClass);
if (exitClass !== void 0 || exitDuration > 0) {
setTimeout(remove, exitDuration);
state.exitTimer = setTimeout(remove, exitDuration);
} else {

@@ -567,8 +571,9 @@ remove();

};
function dismiss() {
finish(false);
function dismiss(options2) {
if (options2?.instant === true) removeNow();
else fadeOut();
}
dismiss.removeNow = () => finish(true);
dismiss.removeNow = removeNow;
active.add(dismiss);
if (duration > 0) state.timer = setTimeout(dismiss, duration);
if (duration > 0) state.timer = setTimeout(fadeOut, duration);
return { el, dismiss };

@@ -575,0 +580,0 @@ }

@@ -25,3 +25,3 @@ import { M as MountResult } from './mount-Bo2qOx25.js';

* a disposer that tears down the current subtree and stops watching the key.
* Pairs with `kerfjs/imperative`: put the widget's setup/teardown on the fresh
* Pairs with `kerfjs/attach`: put the widget's setup/teardown on the fresh
* node, and `remountOn` drives its re-creation.

@@ -37,6 +37,6 @@ */

* subtree. This is where you bind a widget to the new DOM (e.g.
* `imperative(parent.querySelector('.host'), setup)` from `kerfjs/imperative`),
* `attach(parent.querySelector('.host'), setup)` from `kerfjs/attach`),
* because `render` returns a string and has no live node yet. May return a
* cleanup `() => void` that runs before the NEXT remount and on dispose — return
* the disposer from `imperative()` here for synchronous teardown.
* the disposer from `attach()` here for synchronous teardown.
*/

@@ -43,0 +43,0 @@ onMount?: (root: HTMLElement) => (() => void) | void;

@@ -1,5 +0,5 @@

import { mount } from './chunk-4MY2656S.js';
import { mount } from './chunk-LKWAKC2X.js';
import './chunk-QIP723L4.js';
import './chunk-YHH7OUFA.js';
import './chunk-FSAQR6IU.js';
import './chunk-SUPUPSBE.js';
import { effect } from './chunk-3APBEVHF.js';

@@ -6,0 +6,0 @@ import './chunk-GY4XV2UV.js';

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/remount.ts"],"names":[],"mappings":";;;;;;;;;AA0CA,IAAM,KAAA,0BAAe,oBAAoB,CAAA;AASlC,SAAS,UACd,MAAA,EACA,GAAA,EACA,MAAA,EACA,OAAA,GAA0B,EAAC,EACf;AACZ,EAAA,MAAM,EAAE,SAAQ,GAAI,OAAA;AACpB,EAAA,MAAM,UAAU,OAAO,GAAA,KAAQ,UAAA,GAAa,GAAA,GAAM,MAAS,GAAA,CAAI,KAAA;AAC/D,EAAA,IAAI,UAAA,GAA+B,KAAA;AACnC,EAAA,IAAI,YAAA;AACJ,EAAA,IAAI,cAAA;AAEJ,EAAA,SAAS,QAAA,GAAiB;AAIxB,IAAA,IAAI,mBAAmB,MAAA,EAAW;AAChC,MAAA,cAAA,EAAe;AACf,MAAA,cAAA,GAAiB,MAAA;AAAA,IACnB;AACA,IAAA,IAAI,iBAAiB,MAAA,EAAW;AAC9B,MAAA,YAAA,EAAa;AACb,MAAA,YAAA,GAAe,MAAA;AAAA,IACjB;AAGA,IAAA,MAAA,CAAO,eAAA,EAAgB;AAAA,EACzB;AAKA,EAAA,MAAM,SAAA,GAAY,OAAO,MAAM;AAC7B,IAAA,MAAM,OAAO,OAAA,EAAQ;AACrB,IAAA,IAAI,CAAC,MAAA,CAAO,EAAA,CAAG,IAAA,EAAM,UAAU,CAAA,EAAG;AAChC,MAAA,UAAA,GAAa,IAAA;AACb,MAAA,QAAA,EAAS;AACT,MAAA,YAAA,GAAe,KAAA,CAAM,QAAQ,MAAM,CAAA;AACnC,MAAA,cAAA,GAAiB,OAAA,GAAU,MAAM,CAAA,IAAK,MAAA;AAAA,IACxC;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAO,MAAM;AACX,IAAA,SAAA,EAAU;AACV,IAAA,QAAA,EAAS;AAAA,EACX,CAAA;AACF","file":"remount.js","sourcesContent":["/**\n * `kerfjs/remount` — force a subtree to be REPLACED, not morphed, when a key changes.\n *\n * kerf morphs by default, which is almost always right. The exception is a\n * library-owned subtree (a highlighted diff, a chart, an editor) that must be\n * torn down and rebuilt on fresh DOM when its identity changes — so the library\n * re-initializes instead of the morph patching stale internals underneath it.\n * The folk pattern is a monotonic counter spent as `data-key={`gen-${n}`}` on a\n * `data-morph-skip` div; `remountOn` names that pattern.\n *\n * import { remountOn } from 'kerfjs/remount';\n *\n * // Replace the diff pane whenever the file (or diff mode) changes:\n * const stop = remountOn(paneEl, () => fileId.value, () => <DiffView id={fileId.value} />);\n * // same key -> the subtree is left entirely alone\n * // key change -> old subtree + its mounts disposed, a fresh one mounted\n *\n * `remountOn` owns `parent`'s children (like `mount()` / `bindList`). It returns\n * a disposer that tears down the current subtree and stops watching the key.\n * Pairs with `kerfjs/imperative`: put the widget's setup/teardown on the fresh\n * node, and `remountOn` drives its re-creation.\n */\nimport { mount, type MountResult } from './mount.js';\nimport { effect, type ReadonlySignal } from './reactive.js';\n\n/** The key that drives a {@link remountOn}: a signal, or a thunk that reads signals. */\nexport type RemountKey<K> = ReadonlySignal<K> | (() => K);\n\n/** Options for {@link remountOn}. */\nexport interface RemountOptions {\n /**\n * Called after each (re)mount with `parent` — the live, freshly-rendered\n * subtree. This is where you bind a widget to the new DOM (e.g.\n * `imperative(parent.querySelector('.host'), setup)` from `kerfjs/imperative`),\n * because `render` returns a string and has no live node yet. May return a\n * cleanup `() => void` that runs before the NEXT remount and on dispose — return\n * the disposer from `imperative()` here for synchronous teardown.\n */\n onMount?: (root: HTMLElement) => (() => void) | void;\n}\n\n/** Distinguishes \"no key seen yet\" from any real key (including `undefined`). */\nconst UNSET = Symbol('kerf.remount.unset');\n\n/**\n * Watch `key` and, whenever it changes (by `Object.is`), dispose the current\n * subtree + its mounts and render a fresh one into `parent` via `mount(render)`.\n * An unchanged key leaves the subtree untouched. `options.onMount(parent)` runs\n * after each (re)mount to bind widgets to the fresh DOM. Returns a disposer that\n * tears down the current subtree and stops watching.\n */\nexport function remountOn<K>(\n parent: HTMLElement,\n key: RemountKey<K>,\n render: () => MountResult,\n options: RemountOptions = {},\n): () => void {\n const { onMount } = options;\n const readKey = typeof key === 'function' ? key : (): K => key.value;\n let currentKey: K | typeof UNSET = UNSET;\n let disposeMount: (() => void) | undefined;\n let onMountCleanup: (() => void) | undefined;\n\n function tearDown(): void {\n // Run the onMount cleanup BEFORE tearing down the DOM, so a synchronous\n // teardown (e.g. an imperative() disposer returned from onMount) fires while\n // its node is still attached.\n if (onMountCleanup !== undefined) {\n onMountCleanup();\n onMountCleanup = undefined;\n }\n if (disposeMount !== undefined) {\n disposeMount();\n disposeMount = undefined;\n }\n // Owning parent's children: clear whatever the old mount left so widgets\n // under it see a real removal (their MutationObserver teardown fires).\n parent.replaceChildren();\n }\n\n // The outer effect tracks ONLY the key. `mount()` starts its own independent\n // effect for `render`, so render's signal reads attach there, not here — the\n // key is the sole dependency that triggers a remount.\n const stopWatch = effect(() => {\n const next = readKey();\n if (!Object.is(next, currentKey)) {\n currentKey = next;\n tearDown();\n disposeMount = mount(parent, render);\n onMountCleanup = onMount?.(parent) ?? undefined;\n }\n });\n\n return () => {\n stopWatch();\n tearDown();\n };\n}\n"]}
{"version":3,"sources":["../src/remount.ts"],"names":[],"mappings":";;;;;;;;;AA0CA,IAAM,KAAA,0BAAe,oBAAoB,CAAA;AASlC,SAAS,UACd,MAAA,EACA,GAAA,EACA,MAAA,EACA,OAAA,GAA0B,EAAC,EACf;AACZ,EAAA,MAAM,EAAE,SAAQ,GAAI,OAAA;AACpB,EAAA,MAAM,UAAU,OAAO,GAAA,KAAQ,UAAA,GAAa,GAAA,GAAM,MAAS,GAAA,CAAI,KAAA;AAC/D,EAAA,IAAI,UAAA,GAA+B,KAAA;AACnC,EAAA,IAAI,YAAA;AACJ,EAAA,IAAI,cAAA;AAEJ,EAAA,SAAS,QAAA,GAAiB;AAIxB,IAAA,IAAI,mBAAmB,MAAA,EAAW;AAChC,MAAA,cAAA,EAAe;AACf,MAAA,cAAA,GAAiB,MAAA;AAAA,IACnB;AACA,IAAA,IAAI,iBAAiB,MAAA,EAAW;AAC9B,MAAA,YAAA,EAAa;AACb,MAAA,YAAA,GAAe,MAAA;AAAA,IACjB;AAGA,IAAA,MAAA,CAAO,eAAA,EAAgB;AAAA,EACzB;AAKA,EAAA,MAAM,SAAA,GAAY,OAAO,MAAM;AAC7B,IAAA,MAAM,OAAO,OAAA,EAAQ;AACrB,IAAA,IAAI,CAAC,MAAA,CAAO,EAAA,CAAG,IAAA,EAAM,UAAU,CAAA,EAAG;AAChC,MAAA,UAAA,GAAa,IAAA;AACb,MAAA,QAAA,EAAS;AACT,MAAA,YAAA,GAAe,KAAA,CAAM,QAAQ,MAAM,CAAA;AACnC,MAAA,cAAA,GAAiB,OAAA,GAAU,MAAM,CAAA,IAAK,MAAA;AAAA,IACxC;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAO,MAAM;AACX,IAAA,SAAA,EAAU;AACV,IAAA,QAAA,EAAS;AAAA,EACX,CAAA;AACF","file":"remount.js","sourcesContent":["/**\n * `kerfjs/remount` — force a subtree to be REPLACED, not morphed, when a key changes.\n *\n * kerf morphs by default, which is almost always right. The exception is a\n * library-owned subtree (a highlighted diff, a chart, an editor) that must be\n * torn down and rebuilt on fresh DOM when its identity changes — so the library\n * re-initializes instead of the morph patching stale internals underneath it.\n * The folk pattern is a monotonic counter spent as `data-key={`gen-${n}`}` on a\n * `data-morph-skip` div; `remountOn` names that pattern.\n *\n * import { remountOn } from 'kerfjs/remount';\n *\n * // Replace the diff pane whenever the file (or diff mode) changes:\n * const stop = remountOn(paneEl, () => fileId.value, () => <DiffView id={fileId.value} />);\n * // same key -> the subtree is left entirely alone\n * // key change -> old subtree + its mounts disposed, a fresh one mounted\n *\n * `remountOn` owns `parent`'s children (like `mount()` / `bindList`). It returns\n * a disposer that tears down the current subtree and stops watching the key.\n * Pairs with `kerfjs/attach`: put the widget's setup/teardown on the fresh\n * node, and `remountOn` drives its re-creation.\n */\nimport { mount, type MountResult } from './mount.js';\nimport { effect, type ReadonlySignal } from './reactive.js';\n\n/** The key that drives a {@link remountOn}: a signal, or a thunk that reads signals. */\nexport type RemountKey<K> = ReadonlySignal<K> | (() => K);\n\n/** Options for {@link remountOn}. */\nexport interface RemountOptions {\n /**\n * Called after each (re)mount with `parent` — the live, freshly-rendered\n * subtree. This is where you bind a widget to the new DOM (e.g.\n * `attach(parent.querySelector('.host'), setup)` from `kerfjs/attach`),\n * because `render` returns a string and has no live node yet. May return a\n * cleanup `() => void` that runs before the NEXT remount and on dispose — return\n * the disposer from `attach()` here for synchronous teardown.\n */\n onMount?: (root: HTMLElement) => (() => void) | void;\n}\n\n/** Distinguishes \"no key seen yet\" from any real key (including `undefined`). */\nconst UNSET = Symbol('kerf.remount.unset');\n\n/**\n * Watch `key` and, whenever it changes (by `Object.is`), dispose the current\n * subtree + its mounts and render a fresh one into `parent` via `mount(render)`.\n * An unchanged key leaves the subtree untouched. `options.onMount(parent)` runs\n * after each (re)mount to bind widgets to the fresh DOM. Returns a disposer that\n * tears down the current subtree and stops watching.\n */\nexport function remountOn<K>(\n parent: HTMLElement,\n key: RemountKey<K>,\n render: () => MountResult,\n options: RemountOptions = {},\n): () => void {\n const { onMount } = options;\n const readKey = typeof key === 'function' ? key : (): K => key.value;\n let currentKey: K | typeof UNSET = UNSET;\n let disposeMount: (() => void) | undefined;\n let onMountCleanup: (() => void) | undefined;\n\n function tearDown(): void {\n // Run the onMount cleanup BEFORE tearing down the DOM, so a synchronous\n // teardown (e.g. an attach() disposer returned from onMount) fires while\n // its node is still attached.\n if (onMountCleanup !== undefined) {\n onMountCleanup();\n onMountCleanup = undefined;\n }\n if (disposeMount !== undefined) {\n disposeMount();\n disposeMount = undefined;\n }\n // Owning parent's children: clear whatever the old mount left so widgets\n // under it see a real removal (their MutationObserver teardown fires).\n parent.replaceChildren();\n }\n\n // The outer effect tracks ONLY the key. `mount()` starts its own independent\n // effect for `render`, so render's signal reads attach there, not here — the\n // key is the sole dependency that triggers a remount.\n const stopWatch = effect(() => {\n const next = readKey();\n if (!Object.is(next, currentKey)) {\n currentKey = next;\n tearDown();\n disposeMount = mount(parent, render);\n onMountCleanup = onMount?.(parent) ?? undefined;\n }\n });\n\n return () => {\n stopWatch();\n tearDown();\n };\n}\n"]}

@@ -1,6 +0,6 @@

import { mount } from './chunk-4MY2656S.js';
import { mount } from './chunk-LKWAKC2X.js';
import { delegate } from './chunk-KEZTD6H4.js';
import './chunk-QIP723L4.js';
import './chunk-YHH7OUFA.js';
import './chunk-FSAQR6IU.js';
import './chunk-SUPUPSBE.js';
import { effect } from './chunk-3APBEVHF.js';

@@ -7,0 +7,0 @@ import './chunk-GY4XV2UV.js';

{
"name": "kerfjs",
"version": "4.2.0-beta.4",
"version": "4.2.0-beta.5",
"description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",

@@ -95,5 +95,5 @@ "type": "module",

},
"./imperative": {
"types": "./dist/imperative.d.ts",
"import": "./dist/imperative.js"
"./attach": {
"types": "./dist/attach.d.ts",
"import": "./dist/attach.js"
},

@@ -100,0 +100,0 @@ "./ai/*": "./ai/*"

import { itemVersion } from './chunk-QIP723L4.js';
import { parseRowTemplate, rowContractError, parseSingleRow, collectTemplateChildren } from './chunk-YHH7OUFA.js';
import { captureRowBindings, listSafeHtml, boundTextNodeOf, syncFormProp, newBindingContext, wireBindings, disposeRowBindings, isSafeHtml, wireRowBindings, _setBindingContext, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX, carryOrRewireRowBindings, granularListSafeHtml } from './chunk-FSAQR6IU.js';
import { effect } from './chunk-3APBEVHF.js';
import { LIST_MARKER_PREFIX, flattenWithoutListItems, collectLists, flatten } from './chunk-GY4XV2UV.js';
import { devHooks } from './chunk-VVDJLWMP.js';
// src/list-render-state.ts
function deriveListRenderState(bindingCount) {
if (bindingCount === void 0) return "unbound";
return bindingCount === 0 ? "empty" : "bound";
}
function decideListPath(state, patches, snapshotLength, previousBindingCount) {
if (state === "unbound") return { path: "snapshot", reason: "first-render" };
if (state === "empty") return { path: "snapshot", reason: "empty-binding" };
if (patches.length === 0) return { path: "snapshot", reason: "no-patches" };
let netDelta = 0;
for (const p of patches) {
if (p.type === "insert") netDelta += 1;
else if (p.type === "remove") netDelta -= 1;
else if (p.type === "replace") return { path: "snapshot", reason: "replace" };
}
const count = previousBindingCount ?? 0;
if (count + netDelta !== snapshotLength) {
return { path: "snapshot", reason: "count-drift" };
}
return { path: "granular" };
}
// src/each.ts
var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
function isArraySignal(value) {
return typeof value === "object" && value !== null && value[ARRAY_SIGNAL_BRAND] === true;
}
var context = null;
var renderingRow = false;
function inRowScope(fn) {
const prev = renderingRow;
renderingRow = true;
try {
return fn();
} finally {
renderingRow = prev;
}
}
function _setRenderContext(c) {
context = c;
}
function _resetCallOrderListState(ctx) {
const isCallOrderId = (id) => !id.startsWith("k:");
for (const map of [ctx.caches, ctx.bindingCounts, ctx.bindingSources]) {
for (const id of Array.from(map.keys())) {
if (isCallOrderId(id)) map.delete(id);
}
}
}
function isEachOptions(v) {
return typeof v === "object" && v !== null;
}
var VALID_KEY = /^[A-Za-z0-9_.:/-]+$/;
function assertValidKey(key) {
if (typeof key !== "string" || !VALID_KEY.test(key) || key.includes("--")) {
throw new Error(
`each(): invalid list key ${JSON.stringify(key)}. A key must be a non-empty string of letters, digits, or _ . : / - (and may not contain "--"), because kerf writes it into the list's marker comment in the DOM. Use a short stable identifier, e.g. { key: 'results' }.`
);
}
}
function claimKey(ctx, key) {
assertValidKey(key);
if (renderingRow) {
throw new Error(
`each(): list key ${JSON.stringify(key)} was used by an each() inside a row render. A nested each() is not reconciled \u2014 the row is flattened to HTML, so the inner list never binds and would render as static markup. Render the inner collection with plain .map() (it re-renders with its row), or restructure to a flat list.`
);
}
if (ctx.keysThisRender.has(key)) {
throw new Error(
`each(): duplicate list key ${JSON.stringify(key)}. Every keyed each() in a mount must have its own key \u2014 two lists sharing one would share the same cache, binding and DOM anchor. Give each list a distinct key.`
);
}
ctx.keysThisRender.add(key);
return `k:${key}`;
}
function each(items, render, cacheKeyOrOptions) {
const useOptions = isEachOptions(cacheKeyOrOptions);
const cacheKey = useOptions ? cacheKeyOrOptions.cacheKey : cacheKeyOrOptions;
const listKey = useOptions ? cacheKeyOrOptions.key : void 0;
if (isArraySignal(items) && context !== null) {
return eachGranular(items, render, cacheKey, listKey);
}
const snapshotItems = isArraySignal(items) ? items.value : items;
return eachSnapshot(snapshotItems, render, cacheKey, listKey);
}
function eachSnapshot(items, render, cacheKey, listKey) {
let id;
if (context !== null) {
id = listKey !== void 0 ? claimKey(context, listKey) : String(context.counter++);
} else {
id = "orphan";
}
return eachSnapshotById(items, render, cacheKey, id);
}
function assertObjectItem(item, index) {
if (typeof item !== "object" || item === null) {
throw new Error(
`each(): items must be objects (the per-item HTML cache is a WeakMap), got ${item === null ? "null" : typeof item} at index ${index}. Wrap primitives if you need to iterate them, e.g. items.map(v => ({ v })).`
);
}
}
function eachGranular(sig, render, cacheKey, listKey) {
const ctx = context;
const id = listKey !== void 0 ? claimKey(ctx, listKey) : String(ctx.counter++);
const previousBindingCount = ctx.bindingCounts.get(id);
const patches = sig._consumePatches();
const snapshot = sig.value;
const previousSource = ctx.bindingSources.get(id);
const sourceReused = ctx.bindingSources.has(id) && previousSource !== sig;
if (sourceReused && listKey === void 0) ctx.shiftCandidates.push(id);
const decision = sourceReused ? { path: "snapshot" } : decideListPath(
deriveListRenderState(previousBindingCount),
patches,
snapshot.length,
previousBindingCount
);
if (decision.path === "snapshot") {
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
let staleIndexShift = false;
if (render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
const rendered = [];
for (let i = 0; i < previousBindingCount; i++) rendered.push(i);
for (const p of patches) {
if (p.type === "insert") rendered.splice(p.index, 0, p.index);
else if (p.type === "remove") rendered.splice(p.index, 1);
else if (p.type === "move") {
const [moved] = rendered.splice(p.from, 1);
rendered.splice(p.to, 0, moved);
}
}
for (let i = 0; i < rendered.length; i++) {
if (rendered[i] !== i) {
staleIndexShift = true;
break;
}
}
}
if (cacheKey !== void 0) {
const cache2 = ctx.caches.get(id);
for (let i = 0; i < snapshot.length; i++) {
const item = snapshot[i];
const k = cacheKey(item, i);
const cached = cache2.get(item);
if (cached !== void 0 && cached.cacheKey !== k) {
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
}
}
const renderRow = (item, index) => captureRowBindings(() => inRowScope(() => {
const out = render(item, index);
return isSafeHtml(out) ? out.toString() : out;
}));
const internalPatches = new Array(patches.length);
const cache = ctx.caches.get(id);
try {
for (let i = 0; i < patches.length; i++) {
const p = patches[i];
if (p.type === "insert" || p.type === "update") {
assertObjectItem(p.item, p.index);
const { html, bindings } = renderRow(p.item, p.index);
internalPatches[i] = {
type: p.type,
index: p.index,
item: p.item,
html,
bindings
};
cache?.set(p.item, {
cacheKey: cacheKey ? cacheKey(p.item, p.index) : void 0,
html,
bindings,
version: itemVersion(p.item),
index: p.index
});
} else {
internalPatches[i] = p;
}
}
} catch {
ctx.bindingCounts.delete(id);
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
if (staleIndexShift) devHooks.staleIndex?.(id);
return granularListSafeHtml(id, [], internalPatches, sig);
}
function eachSnapshotById(items, render, cacheKey, id, source) {
let cache = null;
if (context !== null) {
let c = context.caches.get(id);
if (c === void 0) {
c = /* @__PURE__ */ new WeakMap();
context.caches.set(id, c);
}
cache = c;
}
const segItems = new Array(items.length);
const seen = /* @__PURE__ */ new Set();
for (let i = 0; i < items.length; i++) {
const item = items[i];
assertObjectItem(item, i);
if (seen.has(item)) {
throw new Error(
`each(): the same object reference appears at multiple indices in items (first seen earlier, again at index ${i}). The per-item HTML cache is keyed on object identity, so duplicate references break the keyed reconciler and can leak DOM nodes on re-render. Use a fresh object per row (e.g. items.map(o => ({ ...o })) before passing to each()).`
);
}
seen.add(item);
const k = cacheKey ? cacheKey(item, i) : void 0;
const version = itemVersion(item);
let html;
let bindings;
const cached = cache !== null ? cache.get(item) : void 0;
if (cached !== void 0 && cached.cacheKey === k && cached.version === version) {
html = cached.html;
bindings = cached.bindings;
if (cached.index !== i && render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
devHooks.staleIndex?.(id);
}
} else {
const captured = captureRowBindings(() => inRowScope(() => {
const out = render(item, i);
return isSafeHtml(out) ? out.toString() : out;
}));
html = captured.html;
bindings = captured.bindings;
if (cache !== null) cache.set(item, { cacheKey: k, html, bindings, version, index: i });
}
segItems[i] = { ref: item, cacheKey: k, html, bindings };
}
if (cacheKey !== void 0) {
devHooks.duplicateCacheKeys?.(id, segItems);
}
return listSafeHtml(id, segItems, source);
}
// src/list-reconcile-focus.ts
function captureFocus(liveParent) {
const active = document.activeElement;
if (active === null || active === document.body) return null;
if (!liveParent.contains(active)) return null;
const el = active;
let selStart = null;
let selEnd = null;
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
try {
selStart = el.selectionStart;
selEnd = el.selectionEnd;
} catch {
}
}
return { el, selStart, selEnd };
}
function restoreFocus(snap) {
if (document.activeElement === snap.el) return;
if (!snap.el.isConnected) return;
snap.el.focus();
if (snap.selStart !== null && snap.selEnd !== null) {
try {
snap.el.setSelectionRange(snap.selStart, snap.selEnd);
} catch {
}
}
}
// src/morph.ts
var ID_KEY_PREFIX = "id:";
var DATA_KEY_PREFIX = "data-key:";
var ELEMENT_NODE = 1;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;
function getNodeKey(node) {
if (node.nodeType !== ELEMENT_NODE) return void 0;
const el = node;
if (el.id !== "") return `${ID_KEY_PREFIX}${el.id}`;
if (el.dataset !== void 0 && el.dataset.key !== void 0) {
return `${DATA_KEY_PREFIX}${el.dataset.key}`;
}
return void 0;
}
var EMPTY_OWNED = /* @__PURE__ */ new Set();
function morph(liveRoot, template, ownedItems = EMPTY_OWNED) {
if (liveRoot == null) {
throw new Error(
'morph: liveRoot is null/undefined \u2014 pass the live element, e.g. morph(document.getElementById("app")!, template). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say Element.'
);
}
const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template);
const focusSnap = captureFocus(liveRoot);
morphChildren(liveRoot, templateEl, ownedItems);
if (focusSnap !== null) restoreFocus(focusSnap);
}
function _morphElement(fromEl, toEl, ownedItems = EMPTY_OWNED) {
morphElement(fromEl, toEl, ownedItems);
}
function isElementNode(t) {
return typeof t === "object" && t !== null && t.nodeType === ELEMENT_NODE;
}
function parseTemplate(liveRoot, template) {
const el = liveRoot.cloneNode(false);
el.innerHTML = String(template);
return el;
}
function protectionTag(node) {
const { dataset } = node;
return (dataset.morphSkip !== void 0 ? "s" : "") + (dataset.morphSkipChildren !== void 0 ? "c" : "") + (dataset.morphPreserve !== void 0 ? "p" : "");
}
var MARKER_PREFIXES = [LIST_MARKER_PREFIX, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX];
function isMarker(node) {
if (node.nodeType !== COMMENT_NODE) return false;
const { data } = node;
return MARKER_PREFIXES.some((prefix) => data.startsWith(prefix));
}
function markersPairable(a, b) {
if (!isMarker(a) && !isMarker(b)) return true;
return a.data === b.data;
}
function skipOwned(node, ownedItems) {
while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) {
node = node.nextSibling;
}
return node;
}
function isListMarker(node) {
return node.nodeType === COMMENT_NODE && node.data.startsWith(LIST_MARKER_PREFIX);
}
function afterListRegion(marker, ownedItems) {
let last = marker;
for (let r = marker.nextSibling; r !== null; r = r.nextSibling) {
if (isListMarker(r)) break;
if (r.nodeType === ELEMENT_NODE && ownedItems.has(r)) last = r;
}
return last.nextSibling;
}
function morphChildren(fromParent, toParent, ownedItems) {
const keyed = /* @__PURE__ */ new Map();
for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue;
const k = getNodeKey(c);
if (k !== void 0) keyed.set(k, c);
}
let fromChild = skipOwned(fromParent.firstChild, ownedItems);
let toChild = toParent.firstChild;
while (toChild !== null) {
const toNext = toChild.nextSibling;
let matched = null;
const toKey = getNodeKey(toChild);
if (toKey !== void 0 && keyed.has(toKey)) {
matched = keyed.get(toKey);
keyed.delete(toKey);
if (matched !== fromChild) {
fromParent.insertBefore(matched, fromChild);
} else {
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
}
}
if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && markersPairable(fromChild, toChild) && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0 && toKey === void 0 && protectionTag(fromChild) === protectionTag(toChild))) {
matched = fromChild;
fromChild = skipOwned(
isListMarker(matched) ? afterListRegion(matched, ownedItems) : fromChild.nextSibling,
ownedItems
);
if (matched.nodeType === COMMENT_NODE && fromChild !== null) {
const owned = boundTextNodeOf(matched);
if (owned !== null && fromChild === owned) {
fromChild = skipOwned(owned.nextSibling, ownedItems);
}
}
}
if (matched === null && toChild.nodeType === ELEMENT_NODE && fromChild !== null && toKey === void 0) {
const toTag = toChild.tagName;
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
if (scan.nodeType !== ELEMENT_NODE) continue;
const el = scan;
if (ownedItems.has(el)) continue;
if (el.tagName !== toTag || getNodeKey(el) !== void 0) continue;
if (protectionTag(el) !== protectionTag(toChild)) continue;
matched = el;
fromParent.insertBefore(el, fromChild);
break;
}
}
if (matched === null && fromChild !== null && toChild.nodeType === COMMENT_NODE && toChild.data.startsWith(LIST_MARKER_PREFIX)) {
const wantData = toChild.data;
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
if (scan.nodeType !== COMMENT_NODE || scan.data !== wantData) continue;
const regionEnd = afterListRegion(scan, ownedItems);
const run = [];
for (let r = scan; r !== null && r !== regionEnd; r = r.nextSibling) {
run.push(r);
}
const focusSnap = captureFocus(fromParent);
for (const node of run) fromParent.insertBefore(node, fromChild);
if (focusSnap !== null) restoreFocus(focusSnap);
matched = scan;
break;
}
}
if (matched !== null) {
morphNode(matched, toChild, ownedItems);
} else {
const cloned = toChild.cloneNode(true);
fromParent.insertBefore(cloned, fromChild);
}
toChild = toNext;
}
while (fromChild !== null) {
const next = fromChild.nextSibling;
if (fromChild.nodeType === ELEMENT_NODE) {
const el = fromChild;
if (!ownedItems.has(el) && el.dataset.morphPreserve === void 0) {
fromParent.removeChild(fromChild);
}
} else {
fromParent.removeChild(fromChild);
}
fromChild = next;
}
}
function morphNode(fromNode, toNode, ownedItems) {
if (fromNode.nodeType === ELEMENT_NODE) {
morphElement(fromNode, toNode, ownedItems);
return;
}
if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) {
const fromText = fromNode;
const toText = toNode;
if (fromText.data !== toText.data) fromText.data = toText.data;
}
}
function morphElement(fromEl, toEl, ownedItems) {
if (fromEl.tagName !== toEl.tagName) {
const replacement = toEl.cloneNode(true);
fromEl.parentNode?.replaceChild(replacement, fromEl);
return;
}
if (fromEl.dataset.morphSkip !== void 0) return;
if (fromEl.isEqualNode(toEl)) return;
if (fromEl === document.activeElement) {
const ce = fromEl.getAttribute("contenteditable");
if (ce !== null && ce.toLowerCase() !== "false") return;
if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
}
morphAttributes(fromEl, toEl);
if (fromEl.dataset.morphSkipChildren !== void 0) return;
const syncTextareaValue = fromEl.tagName === "TEXTAREA" && fromEl !== document.activeElement && fromEl.textContent !== toEl.textContent;
morphChildren(fromEl, toEl, ownedItems);
if (syncTextareaValue) {
fromEl.value = toEl.textContent;
}
}
function isUserAgentOwnedAttr(tagName, name) {
return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG");
}
function morphAttributes(fromEl, toEl) {
const toAttrs = toEl.attributes;
for (let i = 0; i < toAttrs.length; i++) {
const attr = toAttrs[i];
const ns = attr.namespaceURI;
const name = attr.localName;
const value = attr.value;
if (ns !== null) {
if (fromEl.getAttributeNS(ns, name) !== value) {
fromEl.setAttributeNS(ns, attr.name, value);
}
} else if (fromEl.getAttribute(name) !== value) {
fromEl.setAttribute(name, value);
syncFormProp(fromEl, name, value, true);
}
}
const fromAttrs = fromEl.attributes;
const fromTag = fromEl.tagName;
for (let i = fromAttrs.length - 1; i >= 0; i--) {
const attr = fromAttrs[i];
const ns = attr.namespaceURI;
const name = attr.localName;
if (ns !== null) {
if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);
} else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) {
fromEl.removeAttribute(name);
syncFormProp(fromEl, name, "", false);
}
}
}
function isTextInputOrTextarea(el) {
if (el.tagName === "TEXTAREA") return true;
if (el.tagName === "INPUT") {
const type = el.type;
return type === "text" || type === "search" || type === "url" || type === "email" || type === "tel" || type === "password" || type === "";
}
return false;
}
function preserveTextEntryState(fromEl, toEl) {
if (fromEl.tagName === "TEXTAREA" || fromEl.tagName === "INPUT") {
const fromInput = fromEl;
const toInput = toEl;
toInput.value = fromInput.value;
try {
toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd);
} catch {
}
}
}
// src/list-binding.ts
function endAnchor(binding) {
if (binding.items.length > 0) {
return binding.items[binding.items.length - 1].node.nextSibling;
}
return binding.marker.nextSibling;
}
// src/list-reconcile-fast-paths.ts
var LT = 60;
var GT = 62;
var DQUOTE = 34;
var SQUOTE = 39;
var AMP = 38;
var EQ = 61;
var SLASH = 47;
var TEXT_NODE2 = 3;
var ELEMENT_NODE2 = 1;
function isWhitespace(cc) {
return cc === 32 || cc === 9 || cc === 10 || cc === 13;
}
function tryAttributeOnlyFastPath(liveNode, oldHtml, newHtml) {
const oldGt = oldHtml.indexOf(">");
const newGt = newHtml.indexOf(">");
if (oldGt === -1 || newGt === -1) return false;
if (oldHtml.length - oldGt !== newHtml.length - newGt) return false;
if (oldHtml.slice(oldGt) !== newHtml.slice(newGt)) return false;
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
const oldTag = parseOpeningTag(oldHtml, oldGt);
const newTag = parseOpeningTag(newHtml, newGt);
if (oldTag === null || newTag === null) return false;
if (oldTag.tagName !== newTag.tagName) return false;
for (const name of oldTag.attrs.keys()) {
if (name.indexOf(":") !== -1) return false;
}
for (const name of newTag.attrs.keys()) {
if (name.indexOf(":") !== -1) return false;
}
const liveTagUpper = liveNode.tagName;
for (const [name, rawValue] of newTag.attrs) {
const oldValue = oldTag.attrs.get(name);
if (oldValue === rawValue) continue;
const value = unescapeAttrValue(rawValue);
liveNode.setAttribute(name, value);
syncFormProp(liveNode, name, value, true);
}
for (const name of oldTag.attrs.keys()) {
if (newTag.attrs.has(name)) continue;
if (isUserAgentOwnedAttr2(liveTagUpper, name)) continue;
liveNode.removeAttribute(name);
syncFormProp(liveNode, name, "", false);
}
return true;
}
function tryTextContentFastPath(liveNode, oldHtml, newHtml) {
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
let p = 0;
const minLen = Math.min(oldHtml.length, newHtml.length);
while (p < minLen && oldHtml.charCodeAt(p) === newHtml.charCodeAt(p)) p++;
let s = 0;
const maxS = minLen - p;
while (s < maxS && oldHtml.charCodeAt(oldHtml.length - 1 - s) === newHtml.charCodeAt(newHtml.length - 1 - s)) {
s++;
}
const oldWinEnd = oldHtml.length - s;
const newWinEnd = newHtml.length - s;
if (!isPureTextWindow(oldHtml, p, oldWinEnd)) return false;
if (!isPureTextWindow(newHtml, p, newWinEnd)) return false;
if (p === 0) return false;
const boundaryCc = oldHtml.charCodeAt(p - 1);
if (boundaryCc === LT || boundaryCc === DQUOTE || boundaryCc === SQUOTE || boundaryCc === EQ || boundaryCc === AMP) return false;
const textStart = lastIndexOfChar(oldHtml, GT, p - 1);
if (textStart === -1) return false;
const textEnd = oldHtml.indexOf("<", p);
if (textEnd === -1) return false;
if (textEnd < oldWinEnd) return false;
const newTextEnd = textEnd + (newHtml.length - oldHtml.length);
const oldText = oldHtml.slice(textStart + 1, textEnd);
const newText = newHtml.slice(textStart + 1, newTextEnd);
if (oldHtml.lastIndexOf("<!--kfb", textStart) !== -1) return false;
const textIdx = countTextNodesBefore(oldHtml, textStart + 1);
const targetNode = nthTextNodeDescendant(liveNode, textIdx);
if (targetNode === null) return false;
if (targetNode.nodeValue !== oldText) return false;
targetNode.nodeValue = newText;
const host = targetNode.parentNode;
if (host !== null && host.tagName === "TEXTAREA" && host !== document.activeElement) {
host.value = newText;
}
return true;
}
function containsDataMorphSkip(html) {
return html.indexOf("data-morph-skip") !== -1;
}
function isPureTextWindow(html, start, end) {
for (let i = start; i < end; i++) {
const cc = html.charCodeAt(i);
if (cc === LT || cc === GT || cc === DQUOTE || cc === SQUOTE || cc === AMP || cc === EQ) return false;
}
return true;
}
function lastIndexOfChar(html, target, beforeInclusive) {
for (let i = beforeInclusive; i >= 0; i--) {
if (html.charCodeAt(i) === target) return i;
}
return -1;
}
function countTextNodesBefore(html, beforePos) {
let count = 0;
let i = 0;
while (i < beforePos) {
if (html.charCodeAt(i) === LT) {
while (i < beforePos && html.charCodeAt(i) !== GT) i++;
i++;
} else {
const start = i;
while (i < beforePos && html.charCodeAt(i) !== LT) i++;
if (i > start) count++;
}
}
return count;
}
function nthTextNodeDescendant(root, n) {
let count = 0;
let result = null;
function walk(node) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (result !== null) return;
if (c.nodeType === TEXT_NODE2) {
if (count === n) {
result = c;
return;
}
count++;
} else if (c.nodeType === ELEMENT_NODE2) {
walk(c);
}
}
}
walk(root);
return result;
}
function parseOpeningTag(html, gtPos) {
if (html.charCodeAt(0) !== LT) return null;
let i = 1;
let end = gtPos;
if (i < end && html.charCodeAt(end - 1) === SLASH) end -= 1;
const nameStart = i;
while (i < end) {
const cc = html.charCodeAt(i);
if (isWhitespace(cc)) break;
i++;
}
const tagName = html.slice(nameStart, i);
if (tagName.length === 0) return null;
const attrs = /* @__PURE__ */ new Map();
while (i < end) {
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i >= end) break;
const aNameStart = i;
while (i < end) {
const cc = html.charCodeAt(i);
if (cc === EQ || isWhitespace(cc)) break;
i++;
}
const aName = html.slice(aNameStart, i);
if (aName.length === 0) return null;
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i < end && html.charCodeAt(i) === EQ) {
i++;
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i >= end) return null;
const q = html.charCodeAt(i);
if (q !== DQUOTE && q !== SQUOTE) return null;
i++;
const vStart = i;
while (i < end && html.charCodeAt(i) !== q) i++;
if (i >= end) return null;
attrs.set(aName, html.slice(vStart, i));
i++;
} else {
attrs.set(aName, "");
}
}
return { tagName, attrs };
}
function unescapeAttrValue(s) {
if (s.indexOf("&") === -1) return s;
return s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
}
function isUserAgentOwnedAttr2(tagNameUpper, name) {
return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG");
}
// src/list-reconcile-granular.ts
function reconcileGranular(binding, patches) {
const { liveParent } = binding;
const items = binding.items;
const focusSnap = captureFocus(liveParent);
let i = 0;
while (i < patches.length) {
const patch = patches[i];
if (patch.type === "replace") {
i += 1;
continue;
}
if (patch.type === "update") {
let runEnd = i + 1;
while (runEnd < patches.length && patches[runEnd].type === "update") {
runEnd += 1;
}
const runLen = runEnd - i;
if (runLen === 1) {
applySingleUpdate(liveParent, items, patch);
} else {
applyBulkUpdate(liveParent, items, patches, i, runEnd);
}
i = runEnd;
continue;
}
if (patch.type === "insert") {
let runEnd = i + 1;
while (runEnd < patches.length && patches[runEnd].type === "insert" && patches[runEnd].index === patches[runEnd - 1].index + 1) {
runEnd += 1;
}
const runLen = runEnd - i;
if (runLen === 1) {
applySingleInsert(liveParent, items, patch, endAnchor(binding));
} else {
applyBulkInsert(liveParent, items, patches, i, runEnd, endAnchor(binding));
}
i = runEnd;
continue;
}
if (patch.type === "remove") {
const entry = items[patch.index];
disposeRowBindings(entry.bindingDisposers);
liveParent.removeChild(entry.node);
items.splice(patch.index, 1);
i += 1;
continue;
}
if (patch.type === "move") {
const moved = items[patch.from];
let anchorIdx = patch.to;
if (patch.from < patch.to) anchorIdx += 1;
const anchor = anchorIdx < items.length ? items[anchorIdx].node : endAnchor(binding);
liveParent.insertBefore(moved.node, anchor);
items.splice(patch.from, 1);
items.splice(patch.to, 0, moved);
i += 1;
continue;
}
}
if (focusSnap !== null) restoreFocus(focusSnap);
if (items.length > 0) {
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
}
}
function applySingleInsert(liveParent, items, patch, tailAnchor) {
const { html } = patch;
const newNode = parseSingleRow(html, patch.index, liveParent);
const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor;
liveParent.insertBefore(newNode, anchor);
items.splice(patch.index, 0, {
ref: patch.item,
cacheKey: void 0,
html,
node: newNode,
bindings: patch.bindings,
// KF-294: wire the inserted row's fine-grained bindings to its new node.
bindingDisposers: wireRowIfBound(newNode, patch.bindings)
});
}
function wireRowIfBound(node, bindings) {
return bindings !== void 0 && bindings.length > 0 ? wireRowBindings(node, bindings) : void 0;
}
function applySingleUpdate(liveParent, items, patch) {
const { html } = patch;
const oldEntry = items[patch.index];
if (html === oldEntry.html) {
items[patch.index] = reuseBound(patch, html, oldEntry);
return;
}
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, html)) {
items[patch.index] = reuseBound(patch, html, oldEntry);
return;
}
const newNode = parseSingleRow(html, patch.index, liveParent);
applyParsedRowUpdate(liveParent, items, patch, html, newNode);
}
function applyParsedRowUpdate(liveParent, items, patch, html, newNode) {
const oldEntry = items[patch.index];
if (oldEntry.node.tagName === newNode.tagName) {
_morphElement(oldEntry.node, newNode);
items[patch.index] = reuseBound(patch, html, oldEntry);
} else {
disposeRowBindings(oldEntry.bindingDisposers);
liveParent.replaceChild(newNode, oldEntry.node);
items[patch.index] = {
ref: patch.item,
cacheKey: void 0,
html,
node: newNode,
bindings: patch.bindings,
bindingDisposers: wireRowIfBound(newNode, patch.bindings)
};
}
}
function reuseBound(patch, html, oldEntry) {
const kept = carryOrRewireRowBindings(
oldEntry.node,
oldEntry.bindings,
oldEntry.bindingDisposers,
patch.bindings
);
return {
ref: patch.item,
cacheKey: void 0,
html,
node: oldEntry.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
function applyBulkUpdate(liveParent, items, patches, start, end) {
const morphChanges = [];
for (let k = start; k < end; k++) {
const p = patches[k];
const oldEntry = items[p.index];
if (p.html === oldEntry.html) {
items[p.index] = reuseBound(p, p.html, oldEntry);
continue;
}
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, p.html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, p.html)) {
items[p.index] = reuseBound(p, p.html, oldEntry);
continue;
}
morphChanges.push({ patchIdx: k, html: p.html });
}
if (morphChanges.length === 0) return;
const { content, count } = parseRowTemplate(morphChanges.map((c) => c.html).join(""), liveParent);
if (count !== morphChanges.length) {
throw findOffendingChange(patches, morphChanges, liveParent);
}
const newNodes = collectTemplateChildren(content, morphChanges.length);
for (let k = 0; k < morphChanges.length; k++) {
const c = morphChanges[k];
const p = patches[c.patchIdx];
applyParsedRowUpdate(liveParent, items, p, c.html, newNodes[k]);
}
}
function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
const startIdx = patches[start].index;
const htmls = new Array(end - start);
for (let k = start; k < end; k++) {
htmls[k - start] = patches[k].html;
}
const { content, count } = parseRowTemplate(htmls.join(""), liveParent);
if (count !== htmls.length) {
throw findOffendingInsert(patches, start, htmls, liveParent);
}
const newNodes = collectTemplateChildren(content, end - start);
const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor;
liveParent.insertBefore(content, anchor);
const newEntries = new Array(end - start);
for (let k = 0; k < newEntries.length; k++) {
const p = patches[start + k];
newEntries[k] = {
ref: p.item,
cacheKey: void 0,
html: htmls[k],
node: newNodes[k],
bindings: p.bindings,
bindingDisposers: wireRowIfBound(newNodes[k], p.bindings)
// KF-294
};
}
items.splice(startIdx, 0, ...newEntries);
}
function findOffendingInsert(patches, start, htmls, liveParent) {
for (let i = 0; i < htmls.length; i++) {
if (parseRowTemplate(htmls[i], liveParent).count !== 1) {
return rowContractError(patches[start + i].index, htmls[i], liveParent);
}
}
return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug).");
}
function findOffendingChange(patches, changes, liveParent) {
for (const c of changes) {
if (parseRowTemplate(c.html, liveParent).count !== 1) {
return rowContractError(patches[c.patchIdx].index, c.html, liveParent);
}
}
return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug).");
}
// src/list-reconcile-inplace.ts
function tryInPlaceContentUpdate(binding, listSeg) {
const oldItems = binding.items;
const items = listSeg.items;
const n = items.length;
if (n === 0 || n !== oldItems.length) return false;
for (let i = 0; i < n; i++) {
if (items[i].ref !== oldItems[i].ref) return false;
}
const { liveParent } = binding;
const newRecord = new Array(n);
const focusSnap = captureFocus(liveParent);
for (let i = 0; i < n; i++) {
newRecord[i] = updateRowInPlace(liveParent, oldItems[i], items[i], i);
}
if (focusSnap !== null) restoreFocus(focusSnap);
binding.items = newRecord;
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
return true;
}
function updateRowInPlace(liveParent, old, ni, index) {
if (old.html === ni.html || tryAttributeOnlyFastPath(old.node, old.html, ni.html) || tryTextContentFastPath(old.node, old.html, ni.html)) {
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: old.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
const newNode = parseSingleRow(ni.html, index, liveParent);
if (old.node.tagName === newNode.tagName) {
_morphElement(old.node, newNode);
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: old.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
disposeRowBindings(old.bindingDisposers);
liveParent.replaceChild(newNode, old.node);
const fresh = carryOrRewireRowBindings(newNode, void 0, void 0, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: newNode,
bindings: fresh.bindings,
bindingDisposers: fresh.bindingDisposers
};
}
// src/list-reconcile-snapshot.ts
function reconcileSnapshot(binding, listSeg) {
if (tryInPlaceContentUpdate(binding, listSeg)) return;
const { liveParent } = binding;
const { newRecord, prevIdx, removedItems, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
const tailAnchor = endAnchor(binding);
buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent);
const focusSnap = captureFocus(liveParent);
removeOldNodes(liveParent, removedItems);
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
if (focusSnap !== null) restoreFocus(focusSnap);
binding.items = newRecord;
if (newRecord.length > 0) {
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
}
}
function classifyItems(oldItems, listSeg) {
const oldByRef = /* @__PURE__ */ new Map();
for (let i = 0; i < oldItems.length; i++) {
oldByRef.set(oldItems[i].ref, [oldItems[i], i]);
}
const newRecord = new Array(listSeg.items.length);
const prevIdx = new Array(listSeg.items.length);
const removedItems = [];
const freshIndices = [];
const freshHtmls = [];
for (let i = 0; i < listSeg.items.length; i++) {
const ni = listSeg.items[i];
const oi = oldByRef.get(ni.ref);
if (oi !== void 0) {
oldByRef.delete(ni.ref);
if (oi[0].html === ni.html) {
newRecord[i] = oi[0];
prevIdx[i] = oi[1];
continue;
}
removedItems.push(oi[0]);
}
newRecord[i] = {
// `node` placeholder is filled by `buildFreshNodes`; its parse-count
// check guarantees every fresh index gets a real element before use.
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: null,
bindings: ni.bindings
};
prevIdx[i] = -1;
freshIndices.push(i);
freshHtmls.push(ni.html);
}
for (const [, orphan] of oldByRef) removedItems.push(orphan[0]);
return { newRecord, prevIdx, removedItems, freshIndices, freshHtmls };
}
function buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent) {
if (freshHtmls.length === 0) return;
const { content, count } = parseRowTemplate(freshHtmls.join(""), liveParent);
if (count !== freshHtmls.length) {
throw findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent);
}
let node = content.firstElementChild;
for (const idx of freshIndices) {
const next = node.nextElementSibling;
const item = newRecord[idx];
item.node = node;
if (item.bindings !== void 0 && item.bindings.length > 0) {
item.bindingDisposers = wireRowBindings(item.node, item.bindings);
}
node = next;
}
}
function findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent) {
for (let i = 0; i < freshHtmls.length; i++) {
if (parseRowTemplate(freshHtmls[i], liveParent).count !== 1) {
return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html, liveParent);
}
}
return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug).");
}
function removeOldNodes(liveParent, removedItems) {
for (const item of removedItems) {
disposeRowBindings(item.bindingDisposers);
if (item.node.parentElement === liveParent) liveParent.removeChild(item.node);
}
}
function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) {
let nextSibling = tailAnchor;
for (let i = newRecord.length - 1; i >= 0; i--) {
const node = newRecord[i].node;
if (prevIdx[i] === -1 || !stable.has(i)) {
liveParent.insertBefore(node, nextSibling);
}
nextSibling = node;
}
}
function lis(arr) {
const tails = [];
const tailIdx = [];
const prev = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
const v = arr[i];
if (v === -1) {
prev[i] = -1;
continue;
}
let lo = 0;
let hi = tails.length;
while (lo < hi) {
const mid = lo + hi >> 1;
if (tails[mid] < v) lo = mid + 1;
else hi = mid;
}
prev[i] = lo > 0 ? tailIdx[lo - 1] : -1;
tails[lo] = v;
tailIdx[lo] = i;
}
const out = /* @__PURE__ */ new Set();
let k = tailIdx.length > 0 ? tailIdx[tailIdx.length - 1] : -1;
while (k !== -1) {
out.add(k);
k = prev[k];
}
return out;
}
// src/list-reconcile.ts
function reconcileList(binding, listSeg) {
if (listSeg.patches !== void 0 && binding.items.length > 0) {
reconcileGranular(binding, listSeg.patches);
return;
}
reconcileSnapshot(binding, listSeg);
}
// src/mount.ts
var MOUNTED_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.mounted");
var NESTED_MOUNT_MSG = "mount: rootEl is already inside (or contains) a mounted tree. kerf supports one mount per tree \u2014 compose with plain functions that return JSX instead of nesting mounts.";
function isMounted(el) {
return el[MOUNTED_MARKER] === true;
}
function setMounted(el, on) {
if (on) {
el[MOUNTED_MARKER] = true;
} else {
delete el[MOUNTED_MARKER];
}
}
function describeEl(el) {
const tag = el.tagName.toLowerCase();
const id = el.id ? `#${el.id}` : "";
return `<${tag}${id}>`;
}
function assertNotInsideMountedTree(rootEl) {
if (isMounted(rootEl)) {
throw new Error(
`mount: ${describeEl(rootEl)} is already mounted. Call the disposer returned by the first mount() before mounting again. kerf supports one mount per element \u2014 compose with plain functions that return JSX instead of nesting mounts.`
);
}
let ancestor = rootEl.parentElement;
while (ancestor !== null) {
if (isMounted(ancestor)) throw new Error(NESTED_MOUNT_MSG);
ancestor = ancestor.parentElement;
}
const stack = [];
for (let i = 0; i < rootEl.children.length; i++) stack.push(rootEl.children[i]);
while (stack.length > 0) {
const cur = stack.pop();
if (isMounted(cur)) throw new Error(NESTED_MOUNT_MSG);
for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
}
}
function mount(rootEl, render) {
if (rootEl == null) {
throw new Error(
'mount: rootEl is null/undefined \u2014 pass the live element, e.g. mount(document.getElementById("app")!, render). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say HTMLElement.'
);
}
const owner = rootEl.ownerDocument;
if (owner !== document) {
if (owner.defaultView === null) document.adoptNode(rootEl);
}
assertNotInsideMountedTree(rootEl);
setMounted(rootEl, true);
const listenerWarnObserver = devHooks.listenerRebuild?.(rootEl) ?? null;
const bindings = /* @__PURE__ */ new Map();
const renderCtx = {
counter: 0,
caches: /* @__PURE__ */ new Map(),
bindingCounts: /* @__PURE__ */ new Map(),
bindingSources: /* @__PURE__ */ new Map(),
keysThisRender: /* @__PURE__ */ new Set(),
shiftCandidates: [],
warnedShiftIds: /* @__PURE__ */ new Set(),
rebuiltLists: /* @__PURE__ */ new Set()
};
const bindingCtx = newBindingContext();
let bindingDisposers = [];
let prevWiredBindings = [];
let isFirst = true;
let prevStaticHtml = "";
const valueOnlyWarnCtx = { warned: false };
const runRenderPass = () => {
renderCtx.counter = 0;
renderCtx.keysThisRender.clear();
renderCtx.shiftCandidates.length = 0;
bindingCtx.counter = 0;
bindingCtx.list = [];
_setRenderContext(renderCtx);
_setBindingContext(bindingCtx);
try {
return render();
} finally {
_setRenderContext(null);
_setBindingContext(null);
}
};
const disposeEffect = effect(() => {
let result = runRenderPass();
const countChanged = renderCtx.previousCallCount !== void 0 && renderCtx.previousCallCount !== renderCtx.counter;
if (countChanged) {
for (const id of renderCtx.shiftCandidates) {
if (renderCtx.warnedShiftIds.has(id)) continue;
renderCtx.warnedShiftIds.add(id);
devHooks.listIdShift?.(id);
}
_resetCallOrderListState(renderCtx);
result = runRenderPass();
}
let segment = resultToSegment(result);
if (isFirst) {
runFirstRender(rootEl, segment, bindings);
prevStaticHtml = flattenWithoutListItems(segment);
devHooks.parserRepair?.(prevStaticHtml);
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
isFirst = false;
} else {
let nextStaticHtml = runSubsequentRender(
rootEl,
segment,
bindings,
renderCtx,
prevStaticHtml,
valueOnlyWarnCtx
);
if (anyRebuiltListIsGranular(segment, renderCtx.rebuiltLists)) {
for (const id of renderCtx.rebuiltLists) renderCtx.bindingCounts.delete(id);
result = runRenderPass();
segment = resultToSegment(result);
nextStaticHtml = runSubsequentRender(
rootEl,
segment,
bindings,
renderCtx,
prevStaticHtml,
valueOnlyWarnCtx
);
}
if (nextStaticHtml !== prevStaticHtml) {
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
} else {
devHooks.staleBinding?.(prevWiredBindings, bindingCtx.list);
}
prevStaticHtml = nextStaticHtml;
}
const expectedCounts = devHooks.listInvariantsEnabled?.() === true ? /* @__PURE__ */ new Map() : null;
for (const listSeg of collectLists(segment).values()) {
const binding = bindings.get(listSeg.id);
if (binding === void 0) {
throw new Error(
"mount: an each() list appeared in the render output but its marker never reached the live DOM. The most common cause is an each() introduced inside a data-morph-skip subtree on a re-render \u2014 the morph leaves that subtree untouched, so the list can never bind. Move the each() outside the skipped subtree, or remove data-morph-skip from its ancestor."
);
}
reconcileList(binding, listSeg);
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
renderCtx.bindingSources.set(listSeg.id, listSeg.source);
expectedCounts?.set(
listSeg.id,
listSeg.patches !== void 0 && listSeg.source !== void 0 ? listSeg.source.value.length : listSeg.items.length
);
}
renderCtx.previousCallCount = renderCtx.counter;
devHooks.listInvariants?.(rootEl, bindings, expectedCounts ?? void 0);
});
return () => {
disposeEffect();
for (const d of bindingDisposers) d();
bindingDisposers = [];
for (const b of bindings.values()) {
for (const item of b.items) disposeRowBindings(item.bindingDisposers);
}
listenerWarnObserver?.disconnect();
setMounted(rootEl, false);
};
}
function runFirstRender(rootEl, segment, bindings) {
rootEl.innerHTML = flatten(segment, true);
bindListsFromMarkers(rootEl, segment, bindings, true);
}
function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml, valueOnlyWarnCtx) {
renderCtx.rebuiltLists.clear();
const currentStaticHtml = flattenWithoutListItems(segment);
if (currentStaticHtml === prevStaticHtml) {
return prevStaticHtml;
}
devHooks.valueOnlyRerender?.(prevStaticHtml, currentStaticHtml, valueOnlyWarnCtx);
cleanupOrphanBindings(segment, bindings, renderCtx);
const template = rootEl.cloneNode(false);
template.innerHTML = currentStaticHtml;
morph(rootEl, template, collectOwnedItems(bindings));
bindListsFromMarkers(rootEl, segment, bindings, false, renderCtx.rebuiltLists);
return currentStaticHtml;
}
function coerceRenderResult(result) {
if (result === null || result === void 0) return "";
if (result === false || result === true) return "";
return String(result);
}
function resultToSegment(result) {
return isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: coerceRenderResult(result) };
}
function anyRebuiltListIsGranular(segment, rebuilt) {
if (rebuilt.size === 0) return false;
const lists = collectLists(segment);
for (const id of rebuilt) {
if (lists.get(id)?.patches !== void 0) return true;
}
return false;
}
function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems, rebuiltLists) {
const lists = collectLists(segment);
const found = [];
collectComments(rootEl, found);
for (const marker of found) {
if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
const id = marker.data.slice(LIST_MARKER_PREFIX.length);
const existing = bindings.get(id);
if (existing !== void 0) {
if (existing.marker === marker && rootEl.contains(existing.marker)) continue;
for (const item of existing.items) {
disposeRowBindings(item.bindingDisposers);
if (rootEl.contains(item.node)) {
item.node.parentElement?.removeChild(item.node);
}
}
bindings.delete(id);
rebuiltLists?.add(id);
devHooks.listRebind?.(id, marker.parentElement);
}
const listSeg = lists.get(id);
const liveParent = marker.parentElement;
const items = [];
if (inlinedItems) {
let next = marker.nextElementSibling;
for (let i = 0; i < listSeg.items.length && next !== null; i++) {
validateInlinedRowMatch(listSeg.items[i].html, i, next, liveParent);
const rowBindings = listSeg.items[i].bindings;
const bound = {
ref: listSeg.items[i].ref,
cacheKey: listSeg.items[i].cacheKey,
html: listSeg.items[i].html,
node: next,
bindings: rowBindings
};
if (rowBindings !== void 0 && rowBindings.length > 0) {
bound.bindingDisposers = wireRowBindings(next, rowBindings);
}
items.push(bound);
next = next.nextElementSibling;
}
}
const binding = { liveParent, items, marker };
if (items.length > 0) {
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
}
devHooks.eachInMorphSkip?.(id, liveParent, rootEl);
bindings.set(id, binding);
}
}
function validateInlinedRowMatch(expectedHtml, index, boundEl, liveParent) {
if (boundEl.outerHTML === expectedHtml) return;
const { content, count } = parseRowTemplate(expectedHtml, liveParent);
if (count !== 1) throw rowContractError(index, expectedHtml, liveParent);
const expectedTag = content.firstElementChild.tagName;
if (boundEl.tagName !== expectedTag) throw rowStructureError(index, boundEl.tagName, expectedTag);
}
function rowStructureError(index, gotTag, wantTag) {
const got = gotTag.toLowerCase();
const want = wantTag.toLowerCase();
return new Error(
`each(): row ${index} renders <${want}>, but the HTML parser wrapped the rows in <${got}> \u2014 so kerf cannot bind one row per element. This happens when an each() of <${want}> sits directly inside a table: the parser inserts <${got}> around the whole run. Put the each() inside an explicit <${got}> (e.g. <table><${got}>{each(...)}</${got}></table>) so the rows are the direct children kerf binds.`
);
}
function collectOwnedItems(bindings) {
const owned = /* @__PURE__ */ new Set();
for (const b of bindings.values()) {
for (const item of b.items) owned.add(item.node);
}
return owned;
}
function cleanupOrphanBindings(segment, bindings, renderCtx) {
const liveIds = collectLists(segment);
for (const [id, binding] of bindings) {
if (liveIds.has(id)) continue;
for (const item of binding.items) {
disposeRowBindings(item.bindingDisposers);
if (item.node.parentElement !== null) {
item.node.parentElement.removeChild(item.node);
}
}
if (binding.marker.parentElement !== null) {
binding.marker.parentElement.removeChild(binding.marker);
}
bindings.delete(id);
renderCtx.bindingCounts.delete(id);
renderCtx.bindingSources.delete(id);
renderCtx.caches.delete(id);
}
}
function collectComments(node, out) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === Node.COMMENT_NODE) out.push(c);
else if (c.nodeType === Node.ELEMENT_NODE) collectComments(c, out);
}
}
export { each, morph, mount };
//# sourceMappingURL=chunk-4MY2656S.js.map
//# sourceMappingURL=chunk-4MY2656S.js.map

Sorry, the diff of this file is too big to display

import { effect, isSignal } from './chunk-3APBEVHF.js';
import { flatten, wrapWithTags, mergeChildSegments } from './chunk-GY4XV2UV.js';
import { devHooks } from './chunk-VVDJLWMP.js';
// src/utils/syncFormProp.ts
function syncFormProp(el, name, value, present) {
const tag = el.tagName;
if (name === "checked") {
if (tag === "INPUT") el.checked = present;
} else if (name === "value") {
if (tag === "INPUT" && el !== document.activeElement) {
el.value = present ? value : "";
}
} else if (name === "selected") {
if (tag === "OPTION") el.selected = present;
}
}
// src/utils/urlScreen.ts
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction", "action", "data"]);
var DANGEROUS_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript"]);
var INERT_JAVASCRIPT_URLS = /* @__PURE__ */ new Set([
"javascript:",
"javascript:;",
"javascript:void(0)",
"javascript:void(0);",
"javascript:void 0",
"javascript:void 0;"
]);
var CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
function normalizeUrl(value) {
return value.replace(CONTROL_CHARS, "").replace(/^\s+/, "");
}
function schemeOf(normalized) {
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(normalized);
return m ? m[1].toLowerCase() : null;
}
function isDangerousDataUrl(normalized) {
const media = /^data:([^;,]*)/.exec(normalized.toLowerCase())?.[1].trim() ?? "";
if (media === "" || media === "text/plain" || media === "text/css") return false;
if (media === "image/svg+xml") return true;
if (media.startsWith("image/")) return false;
if (media.startsWith("font/") || media.startsWith("application/font")) return false;
if (media.startsWith("audio/") || media.startsWith("video/")) return false;
return true;
}
function isDangerousUrlValue(name, value) {
if (!URL_ATTRS.has(name)) return false;
const normalized = normalizeUrl(value);
const scheme = schemeOf(normalized);
if (scheme === null) return false;
if (DANGEROUS_SCHEMES.has(scheme)) {
return !INERT_JAVASCRIPT_URLS.has(normalized.trimEnd().toLowerCase());
}
return scheme === "data" && isDangerousDataUrl(normalized);
}
function dangerousUrlWarning(name, value) {
return `dropped dangerous URL value for ${name}=${JSON.stringify(value.slice(0, 80))}. kerf blocks javascript:, vbscript:, and script-executing data: URLs (text/html, image/svg+xml, xml) in href/src/data/formaction/action/xlink:href by default. Wrap in raw() if this is intentional (e.g. bookmarklets), or sanitize upstream.`;
}
function reportDangerousUrl(context2, name, value) {
const message = `${context2}: ${dangerousUrlWarning(name, value)}`;
devHooks.urlScreenThrow?.(message);
console.warn(message);
}
// src/bindings.ts
var BIND_ATTR = "data-kfb";
var TEXT_MARKER_PREFIX = "kfb:";
var BIND_ATTR_ROW = "data-kfbrow";
var ROW_TEXT_PREFIX = "kfbr:";
var context = null;
var rowSink = null;
var rowCounter = 0;
var NO_DISPOSERS = Object.freeze([]);
function newBindingContext() {
return { counter: 0, list: [] };
}
function _setBindingContext(c) {
context = c;
}
function captureRowBindings(renderRow) {
const prevSink = rowSink;
const prevCounter = rowCounter;
rowSink = [];
rowCounter = 0;
try {
const html = renderRow();
return { html, bindings: rowSink };
} finally {
rowSink = prevSink;
rowCounter = prevCounter;
}
}
function bindAttr(attr, signal) {
if (rowSink !== null) {
const id = `a${rowCounter++}`;
rowSink.push({ kind: "attr", id, attr, signal });
return id;
}
if (context !== null) {
const id = `a${context.counter++}`;
context.list.push({ kind: "attr", id, attr, signal });
return id;
}
return null;
}
function bindMarkerAttr() {
return rowSink !== null ? BIND_ATTR_ROW : BIND_ATTR;
}
function bindText(signal) {
if (rowSink !== null) {
const id = `t${rowCounter++}`;
rowSink.push({ kind: "text", id, signal });
return `<!--${ROW_TEXT_PREFIX}${id}-->`;
}
if (context !== null) {
const id = `t${context.counter++}`;
context.list.push({ kind: "text", id, signal });
return `<!--${TEXT_MARKER_PREFIX}${id}-->`;
}
return null;
}
function wireBindings(rootEl, ctx, prevDisposers) {
for (const d of prevDisposers) d();
if (ctx.list.length === 0) return NO_DISPOSERS;
const disposers = [];
wireInto(rootEl, ctx.list, disposers);
return disposers;
}
function wireRowBindings(rowNode, bindings) {
const disposers = new Array(bindings.length);
const rootIds = rowNode.getAttribute(BIND_ATTR_ROW);
let rootIdSet = null;
let descIndex = null;
let textMarkers = null;
for (let i = 0; i < bindings.length; i++) {
const b = bindings[i];
if (b.kind === "attr") {
let onRoot = false;
if (rootIds !== null) {
if (rootIds === b.id) {
onRoot = true;
} else if (rootIds.indexOf(",") !== -1) {
rootIdSet ??= new Set(rootIds.split(","));
onRoot = rootIdSet.has(b.id);
}
}
let el;
if (onRoot) {
el = rowNode;
} else {
descIndex ??= indexAttrEls(rowNode, BIND_ATTR_ROW);
el = descIndex.get(b.id);
}
if (el === void 0) continue;
disposers[i] = attachAttrEffect(el, b.attr, b.signal);
} else {
if (textMarkers === null) {
textMarkers = /* @__PURE__ */ new Map();
collectComments(rowNode, ROW_TEXT_PREFIX, textMarkers);
}
const marker = textMarkers.get(b.id);
if (marker === void 0) continue;
disposers[i] = attachTextEffect(marker, b.signal);
}
}
return disposers;
}
function disposeRowBindings(disposers) {
if (disposers === void 0) return;
for (const d of disposers) d();
}
function carryOrRewireRowBindings(node, oldBindings, oldDisposers, newBindings) {
const oldLen = oldBindings === void 0 ? 0 : oldBindings.length;
const newLen = newBindings === void 0 ? 0 : newBindings.length;
if (oldLen === newLen) {
let same = true;
for (let i = 0; i < newLen; i++) {
if (oldBindings[i].signal !== newBindings[i].signal) {
same = false;
break;
}
}
if (same) return { bindings: oldBindings, bindingDisposers: oldDisposers };
}
disposeRowBindings(oldDisposers);
return {
bindings: newBindings,
bindingDisposers: newLen > 0 ? wireRowBindings(node, newBindings) : void 0
};
}
function wireInto(scope, bindings, disposers) {
const attrEls = indexAttrEls(scope, BIND_ATTR);
const textMarkers = /* @__PURE__ */ new Map();
collectComments(scope, TEXT_MARKER_PREFIX, textMarkers);
for (const b of bindings) {
if (b.kind === "attr") {
const el = attrEls.get(b.id);
if (el === void 0) continue;
disposers.push(attachAttrEffect(el, b.attr, b.signal));
} else {
const marker = textMarkers.get(b.id);
if (marker === void 0) continue;
disposers.push(attachTextEffect(marker, b.signal));
}
}
}
function indexAttrEls(scope, attrName) {
const map = /* @__PURE__ */ new Map();
for (const el of scope.querySelectorAll(`[${attrName}]`)) {
for (const id of el.getAttribute(attrName).split(",")) map.set(id, el);
}
return map;
}
function attachAttrEffect(el, attr, signal) {
return effect(() => setBoundAttr(el, attr, signal.value));
}
var insertedTextNodes = /* @__PURE__ */ new WeakMap();
function boundTextNodeOf(marker) {
const t = insertedTextNodes.get(marker);
return t !== void 0 && marker.nextSibling === t ? t : null;
}
function attachTextEffect(marker, signal) {
let text = insertedTextNodes.get(marker);
if (text === void 0 || marker.nextSibling !== text) {
text = marker.ownerDocument.createTextNode("");
marker.parentNode.insertBefore(text, marker.nextSibling);
insertedTextNodes.set(marker, text);
}
const node = text;
return effect(() => {
node.data = coerceText(signal.value);
});
}
function setBoundAttr(el, name, value) {
if (value == null || value === false) {
el.removeAttribute(name);
syncFormProp(el, name, "", false);
return;
}
if (value === true) {
el.setAttribute(name, "");
syncFormProp(el, name, "", true);
return;
}
if (isSafeHtmlValue(value)) {
el.setAttribute(name, value.__html);
syncFormProp(el, name, value.__html, true);
return;
}
const str = String(value);
if (isDangerousUrlValue(name, str)) {
reportDangerousUrl("kerf binding", name, str);
el.removeAttribute(name);
return;
}
el.setAttribute(name, str);
syncFormProp(el, name, str, true);
}
var SAFE_HTML_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
function isSafeHtmlValue(v) {
return typeof v === "object" && v !== null && v[SAFE_HTML_BRAND] === true;
}
function coerceText(value) {
if (value == null || typeof value === "boolean") return "";
return String(value);
}
function collectComments(node, prefix, out) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === Node.COMMENT_NODE) {
const data = c.data;
if (data.startsWith(prefix)) out.set(data.slice(prefix.length), c);
} else if (c.nodeType === Node.ELEMENT_NODE) {
collectComments(c, prefix, out);
}
}
}
// src/utils/escapeHtml.ts
function escapeHtml(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function escapeAttr(str) {
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// src/utils/jsx-attr-aliases.ts
var ATTR_ALIASES = {
// HTML attributes
className: "class",
htmlFor: "for",
httpEquiv: "http-equiv",
acceptCharset: "accept-charset",
accessKey: "accesskey",
autoCapitalize: "autocapitalize",
autoComplete: "autocomplete",
autoFocus: "autofocus",
autoPlay: "autoplay",
colSpan: "colspan",
contentEditable: "contenteditable",
crossOrigin: "crossorigin",
dateTime: "datetime",
defaultChecked: "checked",
defaultSelected: "selected",
defaultValue: "value",
encType: "enctype",
formAction: "formaction",
formEncType: "formenctype",
formMethod: "formmethod",
formNoValidate: "formnovalidate",
formTarget: "formtarget",
hrefLang: "hreflang",
inputMode: "inputmode",
maxLength: "maxlength",
minLength: "minlength",
noModule: "nomodule",
noValidate: "novalidate",
readOnly: "readonly",
referrerPolicy: "referrerpolicy",
rowSpan: "rowspan",
spellCheck: "spellcheck",
srcDoc: "srcdoc",
srcLang: "srclang",
srcSet: "srcset",
tabIndex: "tabindex",
useMap: "usemap",
// SVG presentation attributes (camelCase → kebab-case)
strokeWidth: "stroke-width",
strokeLinecap: "stroke-linecap",
strokeLinejoin: "stroke-linejoin",
strokeDasharray: "stroke-dasharray",
strokeDashoffset: "stroke-dashoffset",
strokeMiterlimit: "stroke-miterlimit",
strokeOpacity: "stroke-opacity",
fillOpacity: "fill-opacity",
fillRule: "fill-rule",
clipPath: "clip-path",
clipRule: "clip-rule",
colorInterpolation: "color-interpolation",
colorInterpolationFilters: "color-interpolation-filters",
floodColor: "flood-color",
floodOpacity: "flood-opacity",
lightingColor: "lighting-color",
stopColor: "stop-color",
stopOpacity: "stop-opacity",
shapeRendering: "shape-rendering",
imageRendering: "image-rendering",
textRendering: "text-rendering",
pointerEvents: "pointer-events",
vectorEffect: "vector-effect",
paintOrder: "paint-order",
// SVG text/font attributes
fontFamily: "font-family",
fontSize: "font-size",
fontStyle: "font-style",
fontVariant: "font-variant",
fontWeight: "font-weight",
fontStretch: "font-stretch",
textAnchor: "text-anchor",
textDecoration: "text-decoration",
dominantBaseline: "dominant-baseline",
alignmentBaseline: "alignment-baseline",
baselineShift: "baseline-shift",
letterSpacing: "letter-spacing",
wordSpacing: "word-spacing",
writingMode: "writing-mode",
// SVG marker attributes
markerStart: "marker-start",
markerMid: "marker-mid",
markerEnd: "marker-end",
// SVG xlink (legacy but still used)
xlinkHref: "xlink:href",
xlinkShow: "xlink:show",
xlinkActuate: "xlink:actuate",
xlinkType: "xlink:type",
xlinkRole: "xlink:role",
xlinkTitle: "xlink:title",
xlinkArcrole: "xlink:arcrole",
xmlBase: "xml:base",
xmlLang: "xml:lang",
xmlSpace: "xml:space",
xmlnsXlink: "xmlns:xlink"
};
// src/jsx-runtime.ts
var SAFE_HTML_BRAND2 = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
var SafeHtml = class {
__html;
__segment;
// Branded so `isSafeHtml()` recognizes instances from any copy of this module.
[SAFE_HTML_BRAND2] = true;
constructor(input) {
if (typeof input === "string") {
this.__segment = { kind: "static", html: input };
this.__html = input;
} else {
this.__segment = input;
this.__html = flatten(input, false);
}
}
toString() {
return this.__html;
}
};
function isSafeHtml(value) {
return typeof value === "object" && value !== null && value[SAFE_HTML_BRAND2] === true;
}
function raw(html) {
return new SafeHtml(html);
}
function trustedRaw(html) {
return new SafeHtml(html);
}
function listSafeHtml(id, items, source) {
return new SafeHtml({ kind: "list", id, items, source });
}
function granularListSafeHtml(id, items, patches, source) {
return new SafeHtml({ kind: "list", id, items, patches, source });
}
var VOID_TAGS = /* @__PURE__ */ new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"source",
"track",
"wbr"
]);
function toSegment(child) {
if (child == null || typeof child === "boolean") return { kind: "static", html: "" };
if (isSignal(child)) {
const marker = bindText(child);
if (marker !== null) return { kind: "static", html: marker };
const v = child.value;
return { kind: "static", html: v == null || typeof v === "boolean" ? "" : escapeHtml(String(v)) };
}
if (isSafeHtml(child)) {
return child.__segment ?? { kind: "static", html: child.__html };
}
if (typeof child === "string") return { kind: "static", html: escapeHtml(child) };
if (typeof child === "number") return { kind: "static", html: String(child) };
if (Array.isArray(child)) return mergeChildSegments(child.map(toSegment));
const maybeNode = child;
if (typeof maybeNode === "object" && maybeNode !== null && ("nodeType" in maybeNode || "outerHTML" in maybeNode)) {
throw new Error(
"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."
);
}
throw new Error(
`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).`
);
}
function describeValue(v) {
if (Array.isArray(v)) return "array";
if (typeof v === "object" && v !== null) {
const ctor = v.constructor?.name;
return ctor && ctor !== "Object" ? `object (${ctor})` : "object";
}
return typeof v;
}
var SAFE_ATTR_NAME = /^[A-Za-z_:][\w.:-]*$/;
function assertEmittableAttrName(key, name, isFn) {
if (/^on[a-z]/i.test(name)) {
if (isFn) {
throw new Error(
`JSX: inline event handlers like ${key}={fn} are not supported by kerf's JSX \u2192 HTML-string runtime. Use event delegation from the mount root instead:
delegate(rootEl, 'click', '[data-action="..."]', (evt, target) => { ... });
<button data-action="...">click</button>
See docs/5-event-delegation.md for the tier-1/tier-2/tier-3 model.`
);
}
throw new Error(
`JSX: event-handler attribute ${JSON.stringify(key)} is not allowed \u2014 an \`on*\` attribute (whether a string emitted into HTML or a signal bound via setAttribute) installs a live inline handler, an XSS vector. kerf uses event delegation: delegate(rootEl, 'click', '[data-action="..."]', handler). See docs/5-event-delegation.md.`
);
}
if (!SAFE_ATTR_NAME.test(name)) {
throw new Error(
`JSX: invalid attribute name ${JSON.stringify(key)}. Attribute names must be a letter/underscore/colon followed by letters, digits, or "_.:-" (e.g. class, data-id, aria-label, xlink:href). This usually means an untrusted object was spread into JSX ({...obj}) with attacker-controlled keys \u2014 validate keys first.`
);
}
}
function renderAttr(key, value) {
return renderAttrNamed(key, ATTR_ALIASES[key] ?? key, value);
}
function renderAttrNamed(key, name, value) {
if (value == null || value === false) return "";
assertEmittableAttrName(key, name, typeof value === "function");
if (value === true) return ` ${name}`;
let strValue;
if (isSafeHtml(value)) {
strValue = value.__html;
} else if (typeof value === "number") {
strValue = String(value);
} else if (typeof value === "string") {
if (isDangerousUrlValue(name, value)) {
reportDangerousUrl("JSX", name, value);
return "";
}
strValue = escapeAttr(value);
} else {
throw new Error(
`JSX: unsupported value for attribute "${key}" \u2014 got ${describeValue(value)}. Attribute values must be string, number, boolean, null, undefined, or SafeHtml. Did you mean to read .value off a Signal, or stringify the object first?`
);
}
return ` ${name}="${strValue}"`;
}
function jsx(tag, props) {
if (typeof tag === "function") return tag(props);
const { children, ...attrs } = props;
let attrStr = "";
let bindIds = null;
for (const [k, v] of Object.entries(attrs)) {
if (isSignal(v)) {
const name = ATTR_ALIASES[k] ?? k;
assertEmittableAttrName(k, name, false);
const id = bindAttr(name, v);
if (id !== null) {
(bindIds ??= []).push(id);
continue;
}
attrStr += renderAttr(k, v.value);
continue;
}
attrStr += renderAttr(k, v);
}
if (bindIds !== null) attrStr += ` ${bindMarkerAttr()}="${bindIds.join(",")}"`;
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
const childSegment = children != null ? toSegment(children) : { kind: "static", html: "" };
return new SafeHtml(wrapWithTags(childSegment, `<${tag}${attrStr}>`, `</${tag}>`));
}
function Fragment({ children }) {
return new SafeHtml(children != null ? toSegment(children) : { kind: "static", html: "" });
}
function _toSegment(child) {
return toSegment(child);
}
function _renderAttrVerbatim(name, value) {
return renderAttrNamed(name, name, value);
}
export { Fragment, ROW_TEXT_PREFIX, SafeHtml, TEXT_MARKER_PREFIX, _renderAttrVerbatim, _setBindingContext, _toSegment, assertEmittableAttrName, bindAttr, bindMarkerAttr, boundTextNodeOf, captureRowBindings, carryOrRewireRowBindings, disposeRowBindings, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, newBindingContext, raw, syncFormProp, trustedRaw, wireBindings, wireRowBindings };
//# sourceMappingURL=chunk-FSAQR6IU.js.map
//# sourceMappingURL=chunk-FSAQR6IU.js.map

Sorry, the diff of this file is too big to display

/**
* `kerfjs/imperative` — bind a non-kerf widget's lifecycle to a single DOM node.
*
* `data-morph-skip` lets a library own a subtree so kerf won't touch it — but
* nothing manages that widget's LIFECYCLE. You set it up imperatively after
* render and must remember to tear it down when the node is replaced/removed
* (dropping document-level listeners the widget added, etc.). `imperative` closes
* that seam: it's a `useEffect`-with-cleanup bound to one node.
*
* import { imperative } from 'kerfjs/imperative';
*
* imperative(canvasEl, (el) => {
* const chart = D3.mount(el);
* return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)
* });
*
* `setup(node)` runs immediately and may return a teardown function. The teardown
* runs once — whichever comes first — when the node leaves the document (detected
* by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any
* removal triggers it) or when the returned disposer is called. Re-creation is
* NOT handled here: a fresh node is a fresh `imperative()` call — pair it with
* `kerfjs/remount`, which replaces the node and re-runs your render (and thus
* this call) on the new one.
*/
/** The setup callback for {@link imperative}: run against `node`, optionally return a teardown. */
type ImperativeSetup = (node: Element) => (() => void) | void;
/**
* Run `setup(node)` now, and its returned teardown once — when `node` leaves the
* document, or when the returned disposer is called, whichever is first. Returns
* a disposer (idempotent) so a `mount()` / `Scope` can drive teardown explicitly.
*/
declare function imperative(node: Element, setup: ImperativeSetup): () => void;
export { type ImperativeSetup, imperative };
// src/imperative.ts
function imperative(node, setup) {
const teardown = setup(node);
let done = false;
const finish = () => {
if (done) return;
done = true;
observer.disconnect();
if (typeof teardown === "function") teardown();
};
const observer = new MutationObserver(() => {
if (!node.isConnected) finish();
});
observer.observe(node.getRootNode(), { childList: true, subtree: true });
return finish;
}
export { imperative };
//# sourceMappingURL=imperative.js.map
//# sourceMappingURL=imperative.js.map
{"version":3,"sources":["../src/imperative.ts"],"names":[],"mappings":";AAiCO,SAAS,UAAA,CAAW,MAAe,KAAA,EAAoC;AAC5E,EAAA,MAAM,QAAA,GAAW,MAAM,IAAI,CAAA;AAC3B,EAAA,IAAI,IAAA,GAAO,KAAA;AAEX,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,IAAI,IAAA,EAAM;AACV,IAAA,IAAA,GAAO,IAAA;AACP,IAAA,QAAA,CAAS,UAAA,EAAW;AACpB,IAAA,IAAI,OAAO,QAAA,KAAa,UAAA,EAAY,QAAA,EAAS;AAAA,EAC/C,CAAA;AAMA,EAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM;AAC1C,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAa,MAAA,EAAO;AAAA,EAChC,CAAC,CAAA;AACD,EAAA,QAAA,CAAS,OAAA,CAAQ,KAAK,WAAA,EAAY,EAAG,EAAE,SAAA,EAAW,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAEvE,EAAA,OAAO,MAAA;AACT","file":"imperative.js","sourcesContent":["/**\n * `kerfjs/imperative` — bind a non-kerf widget's lifecycle to a single DOM node.\n *\n * `data-morph-skip` lets a library own a subtree so kerf won't touch it — but\n * nothing manages that widget's LIFECYCLE. You set it up imperatively after\n * render and must remember to tear it down when the node is replaced/removed\n * (dropping document-level listeners the widget added, etc.). `imperative` closes\n * that seam: it's a `useEffect`-with-cleanup bound to one node.\n *\n * import { imperative } from 'kerfjs/imperative';\n *\n * imperative(canvasEl, (el) => {\n * const chart = D3.mount(el);\n * return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)\n * });\n *\n * `setup(node)` runs immediately and may return a teardown function. The teardown\n * runs once — whichever comes first — when the node leaves the document (detected\n * by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any\n * removal triggers it) or when the returned disposer is called. Re-creation is\n * NOT handled here: a fresh node is a fresh `imperative()` call — pair it with\n * `kerfjs/remount`, which replaces the node and re-runs your render (and thus\n * this call) on the new one.\n */\n\n/** The setup callback for {@link imperative}: run against `node`, optionally return a teardown. */\nexport type ImperativeSetup = (node: Element) => (() => void) | void;\n\n/**\n * Run `setup(node)` now, and its returned teardown once — when `node` leaves the\n * document, or when the returned disposer is called, whichever is first. Returns\n * a disposer (idempotent) so a `mount()` / `Scope` can drive teardown explicitly.\n */\nexport function imperative(node: Element, setup: ImperativeSetup): () => void {\n const teardown = setup(node);\n let done = false;\n\n const finish = (): void => {\n if (done) return;\n done = true;\n observer.disconnect();\n if (typeof teardown === 'function') teardown();\n };\n\n // Observe the node's live tree (the document when connected) with subtree, so\n // an ANCESTOR removal — not just a direct one — is caught. Each mutation just\n // re-checks `node.isConnected`, which is true until the node (or an ancestor)\n // is removed, so a morph swap / remountOn replacement / manual removal all fire.\n const observer = new MutationObserver(() => {\n if (!node.isConnected) finish();\n });\n observer.observe(node.getRootNode(), { childList: true, subtree: true });\n\n return finish;\n}\n"]}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display