kerfjs 5.0.0-beta.91 → 5.0.0-beta.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +3 -3
- package/ai/cursorrules +1 -1
- package/ai/manifest.json +5 -5
- package/ai/skill.md +1 -1
- package/dist/async.js +3 -3
- package/dist/async.js.map +1 -1
- package/dist/{chunk-TFLNYFDO.js → chunk-2MNLFTTW.js} +80 -52
- package/dist/chunk-2MNLFTTW.js.map +1 -0
- package/dist/{chunk-WKIPLNVO.js → chunk-6LXUC4ZY.js} +29 -2
- package/dist/chunk-6LXUC4ZY.js.map +1 -0
- package/dist/{chunk-XHHGPZRQ.js → chunk-ZNQSWFYU.js} +316 -28
- package/dist/chunk-ZNQSWFYU.js.map +1 -0
- package/dist/dev.js +1 -1
- package/dist/html.js +1 -1
- package/dist/index.js +2 -2
- package/dist/jsx-runtime.d.ts +3 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/list.js +1 -1
- package/dist/overlay.js +2 -2
- package/dist/remount.js +1 -1
- package/dist/router.js +1 -1
- package/dist/scope.js +1 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/dist/chunk-TFLNYFDO.js.map +0 -1
- package/dist/chunk-WKIPLNVO.js.map +0 -1
- package/dist/chunk-XHHGPZRQ.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
8
8
|
|
|
9
9
|
### Fixed
|
|
10
10
|
|
|
11
|
+
- Re-renders of a large page whose rows come from a memoized `each()` now scale with the change instead of the row count. The change-scoped morph now also runs on renders that contain `each()` lists: a change beside the list, such as a details pane that follows the selection, is re-parsed and morphed without visiting the rows, and kerf falls back to the full diff when the change sits in the element that holds the rows. After such a render, the scan for list markers skips the bound rows. `SafeHtml` also builds its HTML string on first read instead of at construction, so each element above a list no longer copies every row's markup. A one-row selection on a 300-row memoized keyed page drops from about 3.7 ms to 0.3 ms per render in Chromium (about 29 ms to 3 ms in happy-dom). An attribute set imperatively on an element outside the changed markup now survives such a render, as it already did on list-free pages (`KF-03KWNV`).
|
|
12
|
+
|
|
13
|
+
- On the same kind of page, a mount that also has a fine-grained binding outside the list (for example `class={signal}` on a page wrapper) no longer searches every row for binding markers after a render that changed the page. Rows carry only their own row-scoped markers, so kerf now skips them. A selection flip on the 300-row keyed fixture with one such binding drops from about 3.7 ms to 0.4 ms in Chromium (about 66 ms to 6 ms in happy-dom) (`KF-5S9XPH`).
|
|
14
|
+
|
|
15
|
+
- `wireTabBars` no longer re-measures every tab strip after every DOM mutation in the root it watches. It used to read computed style and layout for each strip whenever anything under the root changed, such as terminal output or a list update. In Hot Sheet's terminal profile that work was the single hottest function. Now only the bars a mutation touches are re-measured: a change inside a bar, a newly inserted bar, or a `dir` change on an element above one. A strip with a pinned tab also re-measures when its own size changes (`KF-8H2MH2`).
|
|
16
|
+
|
|
17
|
+
- `resource().run()` can be called inside an `effect()`. `run()` read the resource's own state through a tracking read, so an effect that re-ran a resource from an input signal subscribed to that state, wrote it, and threw `Cycle detected`. `run()` and its progress and failure paths now read the state untracked; `resource.value` stays a tracking read (`KF-H5PNHK`).
|
|
18
|
+
|
|
19
|
+
- Re-renders of a large list-free page now scale with the change instead of the page. When a `mount()` render has no `each()` list and no fine-grained bindings, kerf compares the previous and next static HTML, finds the innermost element around the change whose markup parses on its own, and re-parses and morphs only that element's changed children; anything it cannot prove identical to a full diff still takes the full diff. A one-row selection on a 300-row, ~1.4 MB page drops from about 23 ms to 13.5 ms per render in Chromium. One documented behavior narrows: an attribute set imperatively on an element whose markup did not change now survives a render that changed something else, as it already did on a no-op render. The core grows by about 2 KB min+gzip (~13 KB to ~15 KB) (`KF-HA8R27`).
|
|
20
|
+
|
|
21
|
+
- The morph no longer strips the defaults a custom element reflects onto its host. Web Awesome reflects `variant`, `appearance`, `size`, `attention`, and similar defaults onto `wa-button`, `wa-tag`, `wa-badge`, `wa-textarea`, and others; a template that omitted them lost them on every re-render that reached the element, and Lit put them straight back with a full update (one row selection on a 300-row page flipped 1,200 attributes). On custom elements the morph now removes an attribute the template omits only when the previous template rendered it; a template that stops rendering an attribute still removes it, and plain elements are unchanged. An attribute set imperatively on a custom element now survives re-renders (`KF-K8S4RX`). Elements kerf creates (a mount's first render, a morph insertion, a new `each()` row) start with their template's record, taken before they connect, so even the first re-render leaves their reflected defaults alone; only an element kerf did not create uses the ordinary rule on its first attribute sync (`KF-G7N4PK`).
|
|
22
|
+
|
|
23
|
+
- `KUI-L017` (exceptional spacing shorthand, such as `<List gap="s" />`) now has one severity everywhere: a review finding. `eslint-plugin-kerfjs` reported it from the `error`-level `ui-css-values` rule, so a deliberate off-scale choice failed lint while `kerf-ui-analyze` queued it for review, and `kerf-ui-doctor` counted every occurrence twice (error plus review) with a `KUI-D003` conflict warning. The finding moves to a new `ui-exceptional-shorthands` rule, `warn` in both `recommended-ui` and `strict-ui`; `ui-css-values` keeps `KUI-L013`–`KUI-L016` at `error`. The doctor reads an ESLint warning for an analyzer review code as that review finding, and the analyzer's `KUI-L014`–`KUI-L017` messages now match ESLint's word for word, so the doctor merges each CSS-value finding into one diagnostic. A new `@kerfjs/ui` contract test fails if any ESLint UI rule's preset severity disagrees with the analyzer's classification of the codes it reports (`KF-P3945Y`).
|
|
24
|
+
|
|
25
|
+
- `mount()` no longer walks the whole live tree for `each()` list markers after a re-render that changed the page when the render has no `each()` lists and no list is bound. A large list-free page (one `mount()` with plain `.map()`) paid a full DOM traversal per render for markers kerf never emitted (`KF-VBQJAX`).
|
|
26
|
+
|
|
27
|
+
- Faster JSX attribute serialization on large pages: the runtime remembers, per attribute name, that the name passed the `on*` / malformed-name check and whether it is URL-bearing, instead of re-running those regexes for every attribute of every render, and the URL screen returns early for values without a `:` (no scheme to screen). Rejected names still throw their exact message on every render, and every URL value is still screened. Building a 300-row, ~1.4 MB list-free page drops from about 13.7 ms to 10.8 ms of CPU (`KF-VBQJAX`).
|
|
28
|
+
|
|
29
|
+
- `wireScrollDividers` no longer forces a synchronous layout after a render. A content mutation used to re-measure the scrollers in the next animation frame, and a structural one at once in the mutation callback, both before the browser had laid out the new tree. Both now re-measure once the frame has laid out and before it paints: the wiring toggles an inert, zero-size probe outside the app tree, and its `ResizeObserver` reports the probe after layout. Without `ResizeObserver` the previous timing remains. In a 300-row work area, `measure` self time drops from about 408 ms to under 1 ms over 16 segment switches and 8 row selections; the layout itself now runs once, in the browser's frame (`KF-4XYQ95`).
|
|
30
|
+
|
|
31
|
+
- `wireWorkbench` no longer forces a document-wide style recalculation after ordinary renders and presses. It caches each responsive panel's computed overlay state until a Workbench resize, a root font-size change, or a presentation attribute change on that panel, and its observer re-syncs only for a new Workbench, an added or removed panel, or an attribute change on a panel rather than any `data-collapsed` or `data-presentation` change below the root. In a 300-row work area with Web Awesome menus, the overlay check's forced style time drops from about 150 ms to 0 over 16 segment switches and 8 row selections (`KF-CRRD70`).
|
|
32
|
+
|
|
33
|
+
- Faster `PopupMenu` and `LucideIcon` re-renders in large lists. `LucideIcon` returns the same `SafeHtml` for the same icon and props instead of rebuilding its `<svg>` every render (up to 64 prop variants per icon), and builds its child markup as a Fragment instead of wrapping joined strings in `raw()` (`KF-XW15D6`). `PopupMenu` hashes icon and details markup of 64 or more characters as a memoized digest of the whole string instead of reading every character every render. A 300-row list with a nine-entry menu per row drops from about 12.7 ms to 2.1 ms per render when the icons are rebuilt each render, and from 3.9 ms to 1.3 ms when they are prebuilt. A menu with such a part now has a different `data-key` value than before; the key is still stable across renders, and every other byte of the rendered HTML is unchanged (`KF-V2ZBHT`).
|
|
34
|
+
|
|
35
|
+
- `PopupMenu` hashes its content key with a streaming `charCodeAt` loop instead of joining every field (including icon markup) into one string and iterating its code points, and caches each menu's trigger and entry markup in a bounded least-recently-used cache keyed by that hash and an independent check hash. An unchanged menu now costs one hash pass instead of rendering every entry again for the morph to skip. Rendered HTML, including the `data-key` digest, is byte-identical (`KF-GVCMC7`).
|
|
36
|
+
|
|
11
37
|
- Faster attribute serialization for large static values: `LucideIcon` caches each icon node's rendered child markup by node identity, and the JSX runtime's HTML/attribute escaping skips its replace passes when a value has nothing to escape. A 138-icon render pass drops from about 0.80 ms to 0.45 ms (`KF-JPP0Z5`). `jsx()` also iterates props without the rest-spread and `Object.entries` allocations, and the URL screen checks lowercase attribute names without allocating a lowercased copy, about 25% faster for an attribute-heavy list row (`KF-YHNV84`).
|
|
12
38
|
|
|
13
39
|
- `wireTokenSearchFields` collapses an empty field blurred by a pointer press in the click's own task, after the click reaches the window, instead of a separate task, so the click's render and the collapse share one style recalculation; a click that stops propagating still falls back to the next task (`KF-AS24QP`).
|
|
@@ -20,6 +46,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
20
46
|
|
|
21
47
|
### Documentation
|
|
22
48
|
|
|
49
|
+
- New recipe in the `kerfjs/async` API reference: move heavy app compute (fuzzy search, CSV or markdown parsing, aggregation) into a Worker by having a `resource()` fetcher post a message to the worker and resolve with its reply. It covers request/reply ids, dropping superseded results, worker errors, transferring `ArrayBuffer`s, and keeping the render on the main thread (`KF-04KHT9`).
|
|
50
|
+
|
|
23
51
|
- Clarify for AI consumers that sunken/lowered token names resolve to a translucent tint, and explain intentional nested layers versus duplicate surface paint (`KF-9CM6D8`).
|
|
24
52
|
|
|
25
53
|
### Removed
|
|
@@ -30,6 +58,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
30
58
|
|
|
31
59
|
### Added
|
|
32
60
|
|
|
61
|
+
- **UX Review capture in any `@kerfjs/ui/catalog` app.** The catalog's **Capture region** header link, which starts a UX Review region screenshot preset with the project and the active entry, now ships in the package instead of living in Kerf's own demo. Add `uxReviewPlugin()` from the new Node-only `@kerfjs/ui/vite-ux-review` subpath to the Vite config. Report the project with `wireCatalog`'s new `onUxReviewProject` callback, and pass it to `Catalog`'s new `uxReview` prop. The link appears on local dev and preview servers and stays hidden on static hosting. `uxReviewCaptureUrl` and `loadUxReviewProject` are exported from `@kerfjs/ui/catalog` for custom placements (`KF-XB1G3S`).
|
|
62
|
+
|
|
33
63
|
- Deep-inset Pane content supports `--kui-pane-scrollbar-gutter: stable` for opaque edge-to-edge content, and Pane/Workbench focus rings paint above such scrollers without blocking their controls (`KF-9NVNGT`).
|
|
34
64
|
|
|
35
65
|
- NavStack now has `separator="auto" | "hidden"`. Automatic separators frame a sunken active Pane at the top chrome and optional bottom toolbar; hidden suppresses stack-owned lines (`KF-1XRSRW`).
|
|
@@ -58,6 +88,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
58
88
|
|
|
59
89
|
### Changed
|
|
60
90
|
|
|
91
|
+
- HTML and attribute escaping in the JSX runtime now runs one pass over a value instead of one `replace` pass per special character, about 2x faster on values that need escaping. Output is unchanged (`KF-3SHRTR`).
|
|
92
|
+
|
|
61
93
|
- Rename the internal TabNavigator implementation folder, files, and focused test files from `tab-scaffold` to `tab-navigator`; public imports and rendered DOM hooks stay stable.
|
|
62
94
|
|
|
63
95
|
- `deepInset` now adds the same 8px to the top and bottom of Pane and plain TabNavigator scroll content, including scroll padding; Pane header and footer still own their safe-area edges.
|
package/README.md
CHANGED
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="https://www.npmjs.com/package/kerfjs"><img src="https://img.shields.io/npm/v/kerfjs.svg" alt="npm version" /></a>
|
|
15
|
-
<img src="https://img.shields.io/badge/min%2Bgzip-~
|
|
15
|
+
<img src="https://img.shields.io/badge/min%2Bgzip-~15%20KB-brightgreen.svg" alt="~15 KB minified and gzipped" />
|
|
16
16
|
<img src="https://img.shields.io/npm/l/kerfjs.svg" alt="MIT license" />
|
|
17
17
|
<img src="https://img.shields.io/badge/types-included-3178c6.svg" alt="TypeScript types included" />
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
> ~
|
|
22
|
+
> ~15 KB. No virtual DOM. No compiler. No magic.
|
|
23
23
|
> Reactive UI that touches only the bytes that changed.
|
|
24
24
|
|
|
25
25
|
```ts
|
|
@@ -76,7 +76,7 @@ Write plain `.tsx` and build with your existing esbuild / Vite / tsup — no ext
|
|
|
76
76
|
|
|
77
77
|
## Why Kerf
|
|
78
78
|
|
|
79
|
-
1. **~
|
|
79
|
+
1. **~15 KB in the browser.** ~15 KB minified + gzipped including `@preact/signals-core` (~15 KB with `arraySignal`). The optional setup CLI has its own npm dependencies; they stay out of the browser bundle. No virtual DOM, no scheduler, no concurrent-mode machinery. On the official [krausest benchmark](https://krausest.github.io/js-framework-benchmark/current.html) kerf sits in the same cluster as Vue, Lit, and vanjs; Solid's compiler leads the update-path benchmarks, which kerf doesn't try to match by design — no compiler.
|
|
80
80
|
|
|
81
81
|
2. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it _is_ the DOM.
|
|
82
82
|
|
package/ai/cursorrules
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
# These rules condense `docs/ai/usage-guide.md` into the form Cursor parses.
|
|
12
12
|
|
|
13
|
-
You are writing a UI in kerf — a ~
|
|
13
|
+
You are writing a UI in kerf — a ~15 KB reactive framework (still ~15 KB with `arraySignal`): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler.
|
|
14
14
|
|
|
15
15
|
## Setup
|
|
16
16
|
|
package/ai/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kerfjsVersion": "5.0.0-beta.
|
|
2
|
+
"kerfjsVersion": "5.0.0-beta.93",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"name": "skill",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"bundle": "ai/skill.md",
|
|
8
8
|
"dest": ".claude/skills/kerf-app/SKILL.md",
|
|
9
9
|
"version": "1.25.0",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "bc2622cb12b382118e44aa50d1afb058252e0caf85b7c0ced4433bc0ca76f7c1",
|
|
11
11
|
"history": {
|
|
12
12
|
"1.0.0": "bef982cb743fd4c73e666ee5cf774112e57c93ebb13b24bbacbf08577db0930a",
|
|
13
13
|
"1.1.0": "f2aadb21e33dd389028951ae1ecbd9f8ba68648b3c5beba43fb4dfa2ab29f195",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"1.23.0": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007",
|
|
41
41
|
"1.23.1": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c",
|
|
42
42
|
"1.24.0": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69",
|
|
43
|
-
"1.25.0": "
|
|
43
|
+
"1.25.0": "bc2622cb12b382118e44aa50d1afb058252e0caf85b7c0ced4433bc0ca76f7c1"
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
{
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"bundle": "ai/cursorrules",
|
|
50
50
|
"dest": ".cursorrules",
|
|
51
51
|
"version": "1.23.0",
|
|
52
|
-
"sha256": "
|
|
52
|
+
"sha256": "dad6f677c85bf01760da6694afb7ead1439f0e1f0298ba8dac65de44ba031848",
|
|
53
53
|
"history": {
|
|
54
54
|
"1.0.0": "98f7faff2b7585ef9cc511add42ea5d9bf0cbae8307b066af090e9281136d2ab",
|
|
55
55
|
"1.1.0": "a2ec8c221dc694c98b81009cd2d99f9159a45fda94585de2f54471b227099eda",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"1.21.0": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f",
|
|
81
81
|
"1.21.1": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee",
|
|
82
82
|
"1.22.0": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d",
|
|
83
|
-
"1.23.0": "
|
|
83
|
+
"1.23.0": "dad6f677c85bf01760da6694afb7ead1439f0e1f0298ba8dac65de44ba031848"
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
]
|
package/ai/skill.md
CHANGED
|
@@ -10,7 +10,7 @@ kerf-skill-version: 1.25.0
|
|
|
10
10
|
> project's `.claude/skills/kerf-app/SKILL.md`) so Claude Code activates
|
|
11
11
|
> it whenever you work on a kerf app.
|
|
12
12
|
|
|
13
|
-
kerf is a ~
|
|
13
|
+
kerf is a ~15 KB reactive UI framework (still ~15 KB with `arraySignal`): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler. The whole public surface fits in 17 exports.
|
|
14
14
|
|
|
15
15
|
## Setup
|
|
16
16
|
|
package/dist/async.js
CHANGED
|
@@ -32,7 +32,7 @@ function resource(options = {}) {
|
|
|
32
32
|
const fetcher = maybeFetcher ?? inputOrFetcher;
|
|
33
33
|
const input = maybeFetcher === void 0 ? void 0 : inputOrFetcher;
|
|
34
34
|
const key = cacheKey !== void 0 && maybeFetcher !== void 0 ? cacheKey(input) : void 0;
|
|
35
|
-
const runningData = cacheKey !== void 0 ? key !== void 0 ? cache.get(key) : void 0 : state.
|
|
35
|
+
const runningData = cacheKey !== void 0 ? key !== void 0 ? cache.get(key) : void 0 : state.peek().data;
|
|
36
36
|
const gen = ++generation;
|
|
37
37
|
state.value = {
|
|
38
38
|
status: "running",
|
|
@@ -44,13 +44,13 @@ function resource(options = {}) {
|
|
|
44
44
|
};
|
|
45
45
|
const report = (completed, total) => {
|
|
46
46
|
if (gen === generation) {
|
|
47
|
-
state.value = { ...state.
|
|
47
|
+
state.value = { ...state.peek(), progress: { completed, total } };
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
const fail = (error) => {
|
|
51
51
|
if (gen === generation) {
|
|
52
52
|
state.value = {
|
|
53
|
-
...state.
|
|
53
|
+
...state.peek(),
|
|
54
54
|
status: "failed",
|
|
55
55
|
error,
|
|
56
56
|
progress: void 0,
|
package/dist/async.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/async.ts"],"names":[],"mappings":"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","file":"async.js","sourcesContent":["/**\n * `kerfjs/async` — model async state, with the stale-response guard built in.\n *\n * Every real kerf app reproduces the same shape — `{ status, data, error }` —\n * for loading/error UI, each paired with a hand-rolled generation counter so a\n * slow response can't overwrite a newer one. This subpath blesses exactly that,\n * and no more: you still write the fetch (Node `fetch` for SSR, browser `fetch`\n * client-side), and `.run()` owns the status transitions plus the stale guard.\n *\n * import { resource } from 'kerfjs/async';\n *\n * const users = resource<User[]>();\n * users.run(() => fetch('/api/users').then((r) => r.json()));\n * // render off users.value.status: 'idle' | 'running' | 'completed' | 'failed'\n *\n * Only the LATEST run may resolve the state, so out-of-order responses are\n * dropped automatically. Optional progress: declare the `report` parameter on\n * your fetcher and call it (e.g. from an upload's progress events).\n *\n * Pass an input — `run(input, fetcher)` — to carry which request a run is for\n * through to `value.input` (set for `running`/`completed`/`failed`), so a\n * failure handler can recover the id/params of the run that failed:\n *\n * const diff = resource<Diff, { fileId: string }>();\n * diff.run({ fileId }, (report) => fetchDiff(fileId, report));\n * // on failure: diff.value.status === 'failed' && diff.value.input.fileId\n *\n * For a real SWR-with-cache section, pass `cacheKey` to keep the last value PER\n * input key (switching back to a loaded key paints its cached slice instantly\n * while it revalidates), and read `value.revision` — a counter that bumps only\n * when `data` actually CHANGES (by `equals`, default `Object.is`) — to skip a\n * redundant paint when a poll tick returns identical data:\n *\n * const win = resource<Slice, string>({ cacheKey: (w) => w, equals: sameSlice });\n * win.run(w, () => fetchSlice(w)); // instant cached paint for a revisited w\n */\nimport { signal } from './reactive.js';\n\n/** The lifecycle status of a {@link Resource}. */\nexport type ResourceStatus = 'idle' | 'running' | 'completed' | 'failed';\n\n/** Optional progress for a long-running fetch (uploads, chunked work). */\nexport interface ResourceProgress {\n completed: number;\n total: number;\n}\n\n/** The reactive state a {@link Resource} exposes. */\nexport interface ResourceState<T, I = void> {\n status: ResourceStatus;\n /** The last successful value. Kept across a re-run (stale-while-revalidate) and on failure. */\n data: T | undefined;\n /** The rejection from the most recent failed run. */\n error: unknown;\n /** Latest reported progress while running, or `undefined`. */\n progress: ResourceProgress | undefined;\n /**\n * The input of the LATEST run — the value passed to {@link Resource.run} as\n * `run(input, fetcher)`. Set for `running`, `completed`, AND `failed` (same\n * stale-guard rule as the rest of the state), so an effect can branch on\n * `status === 'failed'` and still know which request failed. `undefined` in\n * `idle`, and for the no-input `run(fetcher)` form.\n */\n input: I | undefined;\n /**\n * A monotonic counter that increments only when `data` actually CHANGES (by\n * the resource's `equals`, default `Object.is`). Compare it against the value\n * you last painted to skip a redundant re-render — e.g. a 30s poll returning\n * identical data leaves `revision` untouched, so you can bail before wiping\n * scroll / sort / hover state. Starts at `0`.\n */\n revision: number;\n}\n\n/** Construction options for {@link resource}. */\nexport interface ResourceOptions<T, I = void> {\n /**\n * Derive a cache key from a run's `input`. When set, the resource keeps the\n * last successful value PER key: starting a run for a key that was loaded\n * before paints its cached slice immediately (still `running`) while the fetch\n * revalidates in the background; a never-loaded key starts with no `data`.\n * Without `cacheKey`, a run keeps the previous run's `data` (single-slot\n * stale-while-revalidate), as before.\n */\n cacheKey?: (input: I) => string;\n /**\n * Equality used to decide whether `data` changed (drives `value.revision`).\n * Default `Object.is`. Pass a structural comparison to dedup a poll that\n * returns a fresh-but-equal object.\n */\n equals?: (a: T, b: T) => boolean;\n}\n\n/**\n * The fetcher passed to {@link Resource.run}. You own the transport. It receives\n * a `report(completed, total)` callback for optional progress — ignore it if you\n * don't need progress (a plain `() => Promise<T>` is assignable here).\n */\nexport type ResourceFetcher<T> = (\n report: (completed: number, total: number) => void,\n) => Promise<T>;\n\n/**\n * An async-state container. Its `value` is a tracking read; drive UI off\n * `value.status`. `I` is the run-input type — parametrize it (`resource<T, I>()`)\n * to carry a typed `run(input, fetcher)` input through to `value.input`.\n */\nexport interface Resource<T, I = void> {\n /** Tracking read of the current {@link ResourceState}. */\n readonly value: ResourceState<T, I>;\n /**\n * Run `fetcher`, driving `idle`/`running` → `completed`/`failed` and guarding\n * against stale responses (only the latest run resolves the state). Never\n * rejects — a failure lands in `value.error`; resolves with the data (or\n * `undefined` on failure) for callers who want to await it.\n */\n run(fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /**\n * Run `fetcher` for a given `input`, exposing it as `value.input` for the\n * `running`/`completed`/`failed` states of THIS run — so a failure handler can\n * recover which request failed. Same stale guard: only the latest run resolves.\n */\n run(input: I, fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /** Reset to `idle` (clearing data/error/progress/input, and the per-key cache) and invalidate any in-flight run. */\n reset(): void;\n /**\n * Read-only: the cached value for a `cacheKey` key, or `undefined` if that key\n * isn't cached (or no `cacheKey` was given). Lets a consumer ask \"is this slice\n * cached?\" without running it (which would mutate state).\n */\n cached(key: string): T | undefined;\n /** Read-only: the keys currently in the per-input cache (`.length` is the cache size). */\n cachedKeys(): string[];\n /** Evict the per-input cache — one `key`, or the whole cache when called with no argument. Does NOT change `value`. */\n clearCache(key?: string): void;\n}\n\n/** Create an async-state {@link Resource}. No per-instance framework state — it's a closure over a signal. */\nexport function resource<T, I = void>(\n options: ResourceOptions<T, I> = {},\n): Resource<T, I> {\n const { cacheKey, equals } = options;\n const eq: (a: T, b: T) => boolean = equals ?? Object.is;\n const cache = new Map<string, T>(); // per-key SWR cache (GC-tied to the resource)\n\n // Revision tracking: `revision` bumps only when `data` changes (by `eq`).\n let revision = 0;\n let lastData: T | undefined;\n const changed = (next: T | undefined): boolean =>\n // undefined transitions are handled by reference; two defined values by `eq`.\n lastData === undefined || next === undefined\n ? lastData !== next\n : !eq(lastData, next);\n const commit = (next: T | undefined): number => {\n if (changed(next)) {\n revision++;\n lastData = next;\n }\n return revision;\n };\n\n const state = signal<ResourceState<T, I>>({\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: 0,\n });\n // Per-resource run counter (closure-local, not module state) — the stale guard.\n let generation = 0;\n\n function run(\n inputOrFetcher: I | ResourceFetcher<T>,\n maybeFetcher?: ResourceFetcher<T>,\n ): Promise<T | undefined> {\n // Two-arg form is (input, fetcher); one-arg form is (fetcher) with no input.\n // A fetcher is always a function, so `maybeFetcher === undefined` uniquely\n // identifies the one-arg call — even when the input value is itself undefined.\n const fetcher = (maybeFetcher ?? inputOrFetcher) as ResourceFetcher<T>;\n const input = (maybeFetcher === undefined ? undefined : inputOrFetcher) as\n I | undefined;\n const key =\n cacheKey !== undefined && maybeFetcher !== undefined\n ? cacheKey(input as I)\n : undefined;\n\n // What `data` shows while running: the cached slice for this key (per-key\n // SWR), or the previous run's data (single-slot SWR) when no cacheKey.\n const runningData =\n cacheKey !== undefined\n ? key !== undefined\n ? cache.get(key)\n : undefined\n : state.value.data;\n\n const gen = ++generation;\n state.value = {\n status: 'running',\n data: runningData,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(runningData),\n };\n\n const report = (completed: number, total: number): void => {\n if (gen === generation) {\n state.value = { ...state.value, progress: { completed, total } };\n }\n };\n\n const fail = (error: unknown): undefined => {\n if (gen === generation) {\n // Keep `data` (and thus `revision`) on failure — stale-while-error.\n state.value = {\n ...state.value,\n status: 'failed',\n error,\n progress: undefined,\n input,\n };\n }\n return undefined;\n };\n\n let pending: Promise<T>;\n try {\n pending = fetcher(report);\n } catch (error: unknown) {\n return Promise.resolve(fail(error));\n }\n\n return pending.then((data) => {\n if (gen === generation) {\n if (key !== undefined) cache.set(key, data);\n state.value = {\n status: 'completed',\n data,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(data),\n };\n }\n return data;\n }, fail);\n }\n\n function reset(): void {\n generation++; // invalidate any in-flight run\n cache.clear();\n state.value = {\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: commit(undefined),\n };\n }\n\n return {\n get value() {\n return state.value;\n },\n run,\n reset,\n cached: (k) => cache.get(k),\n cachedKeys: () => Array.from(cache.keys()),\n clearCache: (k) => {\n if (k === undefined) cache.clear();\n else cache.delete(k);\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/async.ts"],"names":[],"mappings":"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","file":"async.js","sourcesContent":["/**\n * `kerfjs/async` — model async state, with the stale-response guard built in.\n *\n * Every real kerf app reproduces the same shape — `{ status, data, error }` —\n * for loading/error UI, each paired with a hand-rolled generation counter so a\n * slow response can't overwrite a newer one. This subpath blesses exactly that,\n * and no more: you still write the fetch (Node `fetch` for SSR, browser `fetch`\n * client-side), and `.run()` owns the status transitions plus the stale guard.\n *\n * import { resource } from 'kerfjs/async';\n *\n * const users = resource<User[]>();\n * users.run(() => fetch('/api/users').then((r) => r.json()));\n * // render off users.value.status: 'idle' | 'running' | 'completed' | 'failed'\n *\n * Only the LATEST run may resolve the state, so out-of-order responses are\n * dropped automatically. Optional progress: declare the `report` parameter on\n * your fetcher and call it (e.g. from an upload's progress events).\n *\n * Pass an input — `run(input, fetcher)` — to carry which request a run is for\n * through to `value.input` (set for `running`/`completed`/`failed`), so a\n * failure handler can recover the id/params of the run that failed:\n *\n * const diff = resource<Diff, { fileId: string }>();\n * diff.run({ fileId }, (report) => fetchDiff(fileId, report));\n * // on failure: diff.value.status === 'failed' && diff.value.input.fileId\n *\n * For a real SWR-with-cache section, pass `cacheKey` to keep the last value PER\n * input key (switching back to a loaded key paints its cached slice instantly\n * while it revalidates), and read `value.revision` — a counter that bumps only\n * when `data` actually CHANGES (by `equals`, default `Object.is`) — to skip a\n * redundant paint when a poll tick returns identical data:\n *\n * const win = resource<Slice, string>({ cacheKey: (w) => w, equals: sameSlice });\n * win.run(w, () => fetchSlice(w)); // instant cached paint for a revisited w\n */\nimport { signal } from './reactive.js';\n\n/** The lifecycle status of a {@link Resource}. */\nexport type ResourceStatus = 'idle' | 'running' | 'completed' | 'failed';\n\n/** Optional progress for a long-running fetch (uploads, chunked work). */\nexport interface ResourceProgress {\n completed: number;\n total: number;\n}\n\n/** The reactive state a {@link Resource} exposes. */\nexport interface ResourceState<T, I = void> {\n status: ResourceStatus;\n /** The last successful value. Kept across a re-run (stale-while-revalidate) and on failure. */\n data: T | undefined;\n /** The rejection from the most recent failed run. */\n error: unknown;\n /** Latest reported progress while running, or `undefined`. */\n progress: ResourceProgress | undefined;\n /**\n * The input of the LATEST run — the value passed to {@link Resource.run} as\n * `run(input, fetcher)`. Set for `running`, `completed`, AND `failed` (same\n * stale-guard rule as the rest of the state), so an effect can branch on\n * `status === 'failed'` and still know which request failed. `undefined` in\n * `idle`, and for the no-input `run(fetcher)` form.\n */\n input: I | undefined;\n /**\n * A monotonic counter that increments only when `data` actually CHANGES (by\n * the resource's `equals`, default `Object.is`). Compare it against the value\n * you last painted to skip a redundant re-render — e.g. a 30s poll returning\n * identical data leaves `revision` untouched, so you can bail before wiping\n * scroll / sort / hover state. Starts at `0`.\n */\n revision: number;\n}\n\n/** Construction options for {@link resource}. */\nexport interface ResourceOptions<T, I = void> {\n /**\n * Derive a cache key from a run's `input`. When set, the resource keeps the\n * last successful value PER key: starting a run for a key that was loaded\n * before paints its cached slice immediately (still `running`) while the fetch\n * revalidates in the background; a never-loaded key starts with no `data`.\n * Without `cacheKey`, a run keeps the previous run's `data` (single-slot\n * stale-while-revalidate), as before.\n */\n cacheKey?: (input: I) => string;\n /**\n * Equality used to decide whether `data` changed (drives `value.revision`).\n * Default `Object.is`. Pass a structural comparison to dedup a poll that\n * returns a fresh-but-equal object.\n */\n equals?: (a: T, b: T) => boolean;\n}\n\n/**\n * The fetcher passed to {@link Resource.run}. You own the transport. It receives\n * a `report(completed, total)` callback for optional progress — ignore it if you\n * don't need progress (a plain `() => Promise<T>` is assignable here).\n */\nexport type ResourceFetcher<T> = (\n report: (completed: number, total: number) => void,\n) => Promise<T>;\n\n/**\n * An async-state container. Its `value` is a tracking read; drive UI off\n * `value.status`. `I` is the run-input type — parametrize it (`resource<T, I>()`)\n * to carry a typed `run(input, fetcher)` input through to `value.input`.\n */\nexport interface Resource<T, I = void> {\n /** Tracking read of the current {@link ResourceState}. */\n readonly value: ResourceState<T, I>;\n /**\n * Run `fetcher`, driving `idle`/`running` → `completed`/`failed` and guarding\n * against stale responses (only the latest run resolves the state). Never\n * rejects — a failure lands in `value.error`; resolves with the data (or\n * `undefined` on failure) for callers who want to await it.\n */\n run(fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /**\n * Run `fetcher` for a given `input`, exposing it as `value.input` for the\n * `running`/`completed`/`failed` states of THIS run — so a failure handler can\n * recover which request failed. Same stale guard: only the latest run resolves.\n */\n run(input: I, fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /** Reset to `idle` (clearing data/error/progress/input, and the per-key cache) and invalidate any in-flight run. */\n reset(): void;\n /**\n * Read-only: the cached value for a `cacheKey` key, or `undefined` if that key\n * isn't cached (or no `cacheKey` was given). Lets a consumer ask \"is this slice\n * cached?\" without running it (which would mutate state).\n */\n cached(key: string): T | undefined;\n /** Read-only: the keys currently in the per-input cache (`.length` is the cache size). */\n cachedKeys(): string[];\n /** Evict the per-input cache — one `key`, or the whole cache when called with no argument. Does NOT change `value`. */\n clearCache(key?: string): void;\n}\n\n/** Create an async-state {@link Resource}. No per-instance framework state — it's a closure over a signal. */\nexport function resource<T, I = void>(\n options: ResourceOptions<T, I> = {},\n): Resource<T, I> {\n const { cacheKey, equals } = options;\n const eq: (a: T, b: T) => boolean = equals ?? Object.is;\n const cache = new Map<string, T>(); // per-key SWR cache (GC-tied to the resource)\n\n // Revision tracking: `revision` bumps only when `data` changes (by `eq`).\n let revision = 0;\n let lastData: T | undefined;\n const changed = (next: T | undefined): boolean =>\n // undefined transitions are handled by reference; two defined values by `eq`.\n lastData === undefined || next === undefined\n ? lastData !== next\n : !eq(lastData, next);\n const commit = (next: T | undefined): number => {\n if (changed(next)) {\n revision++;\n lastData = next;\n }\n return revision;\n };\n\n const state = signal<ResourceState<T, I>>({\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: 0,\n });\n // Per-resource run counter (closure-local, not module state) — the stale guard.\n let generation = 0;\n\n function run(\n inputOrFetcher: I | ResourceFetcher<T>,\n maybeFetcher?: ResourceFetcher<T>,\n ): Promise<T | undefined> {\n // Two-arg form is (input, fetcher); one-arg form is (fetcher) with no input.\n // A fetcher is always a function, so `maybeFetcher === undefined` uniquely\n // identifies the one-arg call — even when the input value is itself undefined.\n const fetcher = (maybeFetcher ?? inputOrFetcher) as ResourceFetcher<T>;\n const input = (maybeFetcher === undefined ? undefined : inputOrFetcher) as\n I | undefined;\n const key =\n cacheKey !== undefined && maybeFetcher !== undefined\n ? cacheKey(input as I)\n : undefined;\n\n // What `data` shows while running: the cached slice for this key (per-key\n // SWR), or the previous run's data (single-slot SWR) when no cacheKey.\n const runningData =\n cacheKey !== undefined\n ? key !== undefined\n ? cache.get(key)\n : undefined\n : state.peek().data;\n\n const gen = ++generation;\n state.value = {\n status: 'running',\n data: runningData,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(runningData),\n };\n\n const report = (completed: number, total: number): void => {\n if (gen === generation) {\n state.value = { ...state.peek(), progress: { completed, total } };\n }\n };\n\n const fail = (error: unknown): undefined => {\n if (gen === generation) {\n // Keep `data` (and thus `revision`) on failure — stale-while-error.\n state.value = {\n ...state.peek(),\n status: 'failed',\n error,\n progress: undefined,\n input,\n };\n }\n return undefined;\n };\n\n let pending: Promise<T>;\n try {\n pending = fetcher(report);\n } catch (error: unknown) {\n return Promise.resolve(fail(error));\n }\n\n return pending.then((data) => {\n if (gen === generation) {\n if (key !== undefined) cache.set(key, data);\n state.value = {\n status: 'completed',\n data,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(data),\n };\n }\n return data;\n }, fail);\n }\n\n function reset(): void {\n generation++; // invalidate any in-flight run\n cache.clear();\n state.value = {\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: commit(undefined),\n };\n }\n\n return {\n get value() {\n return state.value;\n },\n run,\n reset,\n cached: (k) => cache.get(k),\n cachedKeys: () => Array.from(cache.keys()),\n clearCache: (k) => {\n if (k === undefined) cache.clear();\n else cache.delete(k);\n },\n };\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { effect, isSignal } from './chunk-IHMDBBUJ.js';
|
|
2
|
-
import {
|
|
2
|
+
import { isOverlayHost, flatten, wrapWithTags, mergeChildSegments } from './chunk-4TX6YJDM.js';
|
|
3
3
|
import { devHooks } from './chunk-3C6F6NM4.js';
|
|
4
4
|
|
|
5
5
|
// src/utils/syncFormProp.ts
|
|
@@ -54,9 +54,14 @@ function isDangerousDataUrl(normalized) {
|
|
|
54
54
|
if (media.startsWith("audio/") || media.startsWith("video/")) return false;
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
|
+
function isUrlAttrName(name) {
|
|
58
|
+
return URL_ATTRS.has(/[A-Z]/.test(name) ? name.toLowerCase() : name);
|
|
59
|
+
}
|
|
57
60
|
function isDangerousUrlValue(name, value) {
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
return isUrlAttrName(name) && isDangerousUrl(value);
|
|
62
|
+
}
|
|
63
|
+
function isDangerousUrl(value) {
|
|
64
|
+
if (!value.includes(":")) return false;
|
|
60
65
|
const normalized = normalizeUrl(value);
|
|
61
66
|
const scheme = schemeOf(normalized);
|
|
62
67
|
if (scheme === null) return false;
|
|
@@ -131,12 +136,12 @@ function bindText(signal) {
|
|
|
131
136
|
}
|
|
132
137
|
return null;
|
|
133
138
|
}
|
|
134
|
-
function wireBindings(rootEl, ctx, prevDisposers) {
|
|
139
|
+
function wireBindings(rootEl, ctx, prevDisposers, rows) {
|
|
135
140
|
for (const d of prevDisposers) d();
|
|
136
141
|
if (ctx.list.length === 0) return NO_DISPOSERS;
|
|
137
142
|
const disposers = [];
|
|
138
143
|
try {
|
|
139
|
-
wireInto(rootEl, ctx.list, disposers);
|
|
144
|
+
wireInto(rootEl, ctx.list, disposers, rows);
|
|
140
145
|
} catch (err) {
|
|
141
146
|
for (const d of disposers) d();
|
|
142
147
|
throw err;
|
|
@@ -147,8 +152,8 @@ function wireRowBindings(rowNode, bindings) {
|
|
|
147
152
|
const disposers = [];
|
|
148
153
|
const rootIds = rowNode.getAttribute(BIND_ATTR_ROW);
|
|
149
154
|
let rootIdSet = null;
|
|
150
|
-
let
|
|
151
|
-
|
|
155
|
+
let markers = null;
|
|
156
|
+
const rowMarkers = () => markers ??= indexMarkers(rowNode, BIND_ATTR_ROW, ROW_TEXT_PREFIX);
|
|
152
157
|
try {
|
|
153
158
|
for (let i = 0; i < bindings.length; i++) {
|
|
154
159
|
const b = bindings[i];
|
|
@@ -166,17 +171,12 @@ function wireRowBindings(rowNode, bindings) {
|
|
|
166
171
|
if (onRoot) {
|
|
167
172
|
el = rowNode;
|
|
168
173
|
} else {
|
|
169
|
-
|
|
170
|
-
el = descIndex.get(b.id);
|
|
174
|
+
el = rowMarkers().attrs.get(b.id);
|
|
171
175
|
}
|
|
172
176
|
if (el === void 0) continue;
|
|
173
177
|
disposers.push(attachAttrEffect(el, b.attr, b.signal));
|
|
174
178
|
} else {
|
|
175
|
-
|
|
176
|
-
textMarkers = /* @__PURE__ */ new Map();
|
|
177
|
-
collectComments(rowNode, ROW_TEXT_PREFIX, textMarkers);
|
|
178
|
-
}
|
|
179
|
-
const marker = textMarkers.get(b.id);
|
|
179
|
+
const marker = rowMarkers().texts.get(b.id);
|
|
180
180
|
if (marker === void 0) continue;
|
|
181
181
|
disposers.push(attachTextEffect(marker, b.signal));
|
|
182
182
|
}
|
|
@@ -210,31 +210,35 @@ function carryOrRewireRowBindings(node, oldBindings, oldDisposers, newBindings)
|
|
|
210
210
|
bindingDisposers: newLen > 0 ? wireRowBindings(node, newBindings) : void 0
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
|
-
function wireInto(scope, bindings, disposers) {
|
|
214
|
-
const
|
|
215
|
-
const textMarkers = /* @__PURE__ */ new Map();
|
|
216
|
-
collectComments(scope, TEXT_MARKER_PREFIX, textMarkers);
|
|
213
|
+
function wireInto(scope, bindings, disposers, rows) {
|
|
214
|
+
const markers = indexMarkers(scope, BIND_ATTR, TEXT_MARKER_PREFIX, rows);
|
|
217
215
|
for (const b of bindings) {
|
|
218
216
|
if (b.kind === "attr") {
|
|
219
|
-
const el =
|
|
217
|
+
const el = markers.attrs.get(b.id);
|
|
220
218
|
if (el === void 0) continue;
|
|
221
219
|
disposers.push(attachAttrEffect(el, b.attr, b.signal));
|
|
222
220
|
} else {
|
|
223
|
-
const marker =
|
|
221
|
+
const marker = markers.texts.get(b.id);
|
|
224
222
|
if (marker === void 0) continue;
|
|
225
223
|
disposers.push(attachTextEffect(marker, b.signal));
|
|
226
224
|
}
|
|
227
225
|
}
|
|
228
226
|
}
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
227
|
+
function indexMarkers(node, attrName, textPrefix, skip, out = { attrs: /* @__PURE__ */ new Map(), texts: /* @__PURE__ */ new Map() }) {
|
|
228
|
+
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
|
|
229
|
+
if (c.nodeType === Node.COMMENT_NODE) {
|
|
230
|
+
const data = c.data;
|
|
231
|
+
if (data.startsWith(textPrefix))
|
|
232
|
+
out.texts.set(data.slice(textPrefix.length), c);
|
|
233
|
+
} else if (c.nodeType === Node.ELEMENT_NODE) {
|
|
234
|
+
const el = c;
|
|
235
|
+
if (skip?.has(el) || isOverlayHost(el)) continue;
|
|
236
|
+
const ids = el.getAttribute(attrName);
|
|
237
|
+
if (ids !== null) for (const id of ids.split(",")) out.attrs.set(id, el);
|
|
238
|
+
indexMarkers(el, attrName, textPrefix, skip, out);
|
|
239
|
+
}
|
|
236
240
|
}
|
|
237
|
-
return
|
|
241
|
+
return out;
|
|
238
242
|
}
|
|
239
243
|
function attachAttrEffect(el, attr, signal) {
|
|
240
244
|
return effect(() => setBoundAttr(el, attr, signal.value));
|
|
@@ -289,28 +293,33 @@ function coerceText(value) {
|
|
|
289
293
|
if (value == null || typeof value === "boolean") return "";
|
|
290
294
|
return String(value);
|
|
291
295
|
}
|
|
292
|
-
function collectComments(node, prefix, out) {
|
|
293
|
-
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
|
|
294
|
-
if (c.nodeType === Node.COMMENT_NODE) {
|
|
295
|
-
const data = c.data;
|
|
296
|
-
if (data.startsWith(prefix))
|
|
297
|
-
out.set(data.slice(prefix.length), c);
|
|
298
|
-
} else if (c.nodeType === Node.ELEMENT_NODE && !isOverlayHost(c)) {
|
|
299
|
-
collectComments(c, prefix, out);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
296
|
|
|
304
297
|
// src/utils/escapeHtml.ts
|
|
305
|
-
var
|
|
306
|
-
|
|
298
|
+
var ENTITIES = {
|
|
299
|
+
"&": "&",
|
|
300
|
+
"<": "<",
|
|
301
|
+
">": ">",
|
|
302
|
+
'"': """,
|
|
303
|
+
"'": "'"
|
|
304
|
+
};
|
|
305
|
+
function escapeWith(str, special) {
|
|
306
|
+
special.lastIndex = 0;
|
|
307
|
+
let out = "";
|
|
308
|
+
let last = 0;
|
|
309
|
+
let match;
|
|
310
|
+
while (match = special.exec(str)) {
|
|
311
|
+
out += str.slice(last, match.index) + ENTITIES[match[0]];
|
|
312
|
+
last = special.lastIndex;
|
|
313
|
+
}
|
|
314
|
+
return last ? out + str.slice(last) : str;
|
|
315
|
+
}
|
|
316
|
+
var HTML_SPECIAL = /[&<>"]/g;
|
|
317
|
+
var ATTR_SPECIAL = /[&"'<>]/g;
|
|
307
318
|
function escapeHtml(str) {
|
|
308
|
-
|
|
309
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
319
|
+
return escapeWith(str, HTML_SPECIAL);
|
|
310
320
|
}
|
|
311
321
|
function escapeAttr(str) {
|
|
312
|
-
|
|
313
|
-
return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
322
|
+
return escapeWith(str, ATTR_SPECIAL);
|
|
314
323
|
}
|
|
315
324
|
|
|
316
325
|
// src/utils/jsx-attr-aliases.ts
|
|
@@ -414,19 +423,27 @@ var ATTR_ALIASES = {
|
|
|
414
423
|
// src/jsx-runtime.ts
|
|
415
424
|
var SAFE_HTML_BRAND2 = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
|
|
416
425
|
var SafeHtml = class {
|
|
417
|
-
__html;
|
|
418
426
|
__segment;
|
|
419
427
|
// Branded so `isSafeHtml()` recognizes instances from any copy of this module.
|
|
420
428
|
[SAFE_HTML_BRAND2] = true;
|
|
429
|
+
// The flattened HTML, computed on first read. A structured segment wraps its
|
|
430
|
+
// children's segments rather than their strings, so flattening eagerly made
|
|
431
|
+
// every element above an `each()` copy all of the list's row HTML again —
|
|
432
|
+
// O(depth × rows) per render, while `mount()` reads only the segment.
|
|
433
|
+
#html;
|
|
421
434
|
constructor(input) {
|
|
422
435
|
if (typeof input === "string") {
|
|
423
436
|
this.__segment = { kind: "static", html: input };
|
|
424
|
-
this
|
|
437
|
+
this.#html = input;
|
|
425
438
|
} else {
|
|
426
439
|
this.__segment = input;
|
|
427
|
-
|
|
440
|
+
if (input.kind === "static") this.#html = input.html;
|
|
428
441
|
}
|
|
429
442
|
}
|
|
443
|
+
/** The flattened HTML string (what `toString()` returns). */
|
|
444
|
+
get __html() {
|
|
445
|
+
return this.#html ??= flatten(this.__segment, false);
|
|
446
|
+
}
|
|
430
447
|
toString() {
|
|
431
448
|
return this.__html;
|
|
432
449
|
}
|
|
@@ -518,12 +535,23 @@ See docs/5-event-delegation.md for the tier-1/tier-2/tier-3 model.`
|
|
|
518
535
|
);
|
|
519
536
|
}
|
|
520
537
|
}
|
|
538
|
+
var VALIDATED_ATTR_NAMES = /* @__PURE__ */ new Map();
|
|
539
|
+
var VALIDATED_ATTR_NAMES_MAX = 1024;
|
|
540
|
+
function checkAttrName(key, name, isFn) {
|
|
541
|
+
const cached = VALIDATED_ATTR_NAMES.get(name);
|
|
542
|
+
if (cached !== void 0) return cached;
|
|
543
|
+
assertEmittableAttrName(key, name, isFn);
|
|
544
|
+
const isUrl = isUrlAttrName(name);
|
|
545
|
+
if (VALIDATED_ATTR_NAMES.size < VALIDATED_ATTR_NAMES_MAX)
|
|
546
|
+
VALIDATED_ATTR_NAMES.set(name, isUrl);
|
|
547
|
+
return isUrl;
|
|
548
|
+
}
|
|
521
549
|
function renderAttr(key, value) {
|
|
522
550
|
return renderAttrNamed(key, ATTR_ALIASES[key] ?? key, value);
|
|
523
551
|
}
|
|
524
552
|
function renderAttrNamed(key, name, value) {
|
|
525
553
|
if (value == null || value === false) return "";
|
|
526
|
-
|
|
554
|
+
const isUrlAttr = checkAttrName(key, name, typeof value === "function");
|
|
527
555
|
if (value === true) return ` ${name}`;
|
|
528
556
|
let strValue;
|
|
529
557
|
if (isSafeHtml(value)) {
|
|
@@ -531,7 +559,7 @@ function renderAttrNamed(key, name, value) {
|
|
|
531
559
|
} else if (typeof value === "number") {
|
|
532
560
|
strValue = String(value);
|
|
533
561
|
} else if (typeof value === "string") {
|
|
534
|
-
if (
|
|
562
|
+
if (isUrlAttr && isDangerousUrl(value)) {
|
|
535
563
|
reportDangerousUrl("JSX", name, value);
|
|
536
564
|
return "";
|
|
537
565
|
}
|
|
@@ -553,7 +581,7 @@ function jsx(tag, props) {
|
|
|
553
581
|
const v = props[k];
|
|
554
582
|
if (isSignal(v)) {
|
|
555
583
|
const name = ATTR_ALIASES[k] ?? k;
|
|
556
|
-
|
|
584
|
+
checkAttrName(k, name, false);
|
|
557
585
|
const id = bindAttr(name, v);
|
|
558
586
|
if (id !== null) {
|
|
559
587
|
(bindIds ??= []).push(id);
|
|
@@ -586,4 +614,4 @@ function _renderAttrVerbatim(name, value) {
|
|
|
586
614
|
|
|
587
615
|
export { Fragment, ROW_TEXT_PREFIX, SafeHtml, TEXT_MARKER_PREFIX, _renderAttrVerbatim, _setBindingContext, _toSegment, assertEmittableAttrName, bindAttr, bindMarkerAttr, boundTextNodeOf, captureRowBindings, carryOrRewireRowBindings, disposeRowBindings, escapeHtml, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, newBindingContext, raw, syncFormProp, wireBindings, wireRowBindings };
|
|
588
616
|
|
|
589
|
-
//# sourceMappingURL=chunk-
|
|
617
|
+
//# sourceMappingURL=chunk-2MNLFTTW.js.map
|