kerfjs 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -0
- package/dist/chunk-IZJIKRCE.js +34 -0
- package/dist/chunk-IZJIKRCE.js.map +1 -0
- package/dist/chunk-URMYMSGU.js +196 -0
- package/dist/chunk-URMYMSGU.js.map +1 -0
- package/dist/index.d.ts +16 -10
- package/dist/index.js +29 -63
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +9 -1
- package/dist/jsx-runtime.js +1 -186
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/testing.js +1 -5
- package/dist/testing.js.map +1 -1
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,37 @@ All notable changes to **kerf** are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-05-07
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
- Fix focused contenteditable losing focus/caret during morph
|
|
11
|
+
- Fix `SafeHtml` identity mismatch across entry points caused by dist bundling
|
|
12
|
+
- Fix `clearStoreRegistry` no-op in built output, restoring test isolation
|
|
13
|
+
|
|
14
|
+
## [0.1.2] - 2026-05-07
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
- No user-facing changes; release tooling fixes only.
|
|
18
|
+
|
|
19
|
+
## [0.1.1] - 2026-05-07
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
- This is just a publication script test
|
|
23
|
+
|
|
7
24
|
## [Unreleased]
|
|
8
25
|
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Focused contenteditable was being morphed, clobbering in-progress edits (KF-19).** The docs claimed contenteditable elements got focus + selection preservation alongside `<input>` and `<textarea>`, but the implementation only handled the latter two — a focused contenteditable's typed content was overwritten by morphdom on the next re-render. `mount()` now short-circuits the morph entirely when the active element is a contenteditable (same mechanism as `data-morph-skip`), so the user's edit, caret position, and any multi-range selection survive verbatim. Attribute updates are deferred until the next render after blur — that's the explicit trade-off, and matches what you want for in-progress rich-text editing. `docs/4-render.md` §4.4 and `docs/8-api-reference.md` §8.7 updated to describe the per-element-kind behaviour. The check uses the `contenteditable` attribute directly (the spec's source of truth) rather than the derived `isContentEditable` property, so test environments that don't populate the latter still get correct behaviour.
|
|
29
|
+
- **`clearStoreRegistry` was a no-op in the published bundle (KF-15).** `dist/testing.js` shipped an empty function body. Root cause: `tsup` bundled each entry independently with `splitting: false`, so the testing entry tree-shook the module-level `REGISTRY` array out as unreferenced — leaving `REGISTRY.length = 0` as dead code. Same root cause as KF-14. Fixed by enabling `splitting: true` in `tsup.config.ts`: shared modules now live in chunk files that all entries import, so `defineStore`'s registry and `clearStoreRegistry`'s reference are the same array. Side benefit: the duplicate `SafeHtml` class definition is gone too — there's now exactly one copy across the whole dist. Build output now includes `dist/chunk-*.js` files (covered by the existing `"files": ["dist"]` in `package.json`). New regression test in `tests/dist/store-registry-shared.test.ts` exercises the cross-entry registry from the built bundles.
|
|
30
|
+
- **`SafeHtml` cross-bundle identity (KF-14).** When a consumer's bundler ended up loading two copies of kerf — for example, the barrel (`kerfjs`) and the JSX-runtime entry (`kerfjs/jsx-runtime`) resolving as separate modules — `instanceof SafeHtml` failed inside the JSX runtime because the two `SafeHtml` classes were structurally identical but referentially distinct. The renderer would then throw `JSX: unsupported child of type object (SafeHtml)` on perfectly valid JSX. `SafeHtml` instances now carry a `Symbol.for('kerfjs.SafeHtml')` brand and the runtime checks for the brand instead of using `instanceof`. New unit tests simulate the duplicate-class scenario, and a new `npm run test:dist` job exercises the actual built bundles in CI.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`isSafeHtml(value)` type guard** exported from `kerfjs`. Use this rather than `instanceof SafeHtml` when inspecting JSX values from your own code — it works across module copies.
|
|
35
|
+
- **End-to-end test coverage of the published bundle (KF-16).** New `npm run test:dist:full` re-runs the entire unit + integration suite against `dist/` instead of `src/` via a tiny vitest plugin that rewrites `../../src/<name>.js` imports to the equivalent dist entry point. Wired into the CI `build` job. Combined with the existing `test:dist` (focused dist regression suite), CI now proves the exact bytes we publish pass every test we have, not just the source they were built from.
|
|
36
|
+
- **Four behavioural-guarantee tests (KF-17)** pinning documented contracts that previously had no test: signals are not deep-reactive (§2.6), `batch()` inside an action coalesces notifications (§3.5), `mount()` disposer leaves the rendered DOM in place (§4), and direct event listeners inside `data-morph-skip` subtrees survive parent re-renders (Tier 3, §5).
|
|
37
|
+
|
|
9
38
|
### Changed
|
|
10
39
|
|
|
11
40
|
- **Package renamed from `kerf` to `kerfjs`** on the npm registry. The `kerf` name was rejected by npm's typo-squatting heuristic ("too similar to `keyv`"). The brand is still *kerf* — only the npm identifier changed. Update imports to `from 'kerfjs'`, `tsconfig.json` to `"jsxImportSource": "kerfjs"`, and the install command to `npm install kerfjs`. The GitHub repo and Pages URL (`brianwestphal.github.io/kerf/`) are unchanged.
|
package/README.md
CHANGED
|
@@ -106,6 +106,7 @@ The numbered docs in [`docs/`](./docs/) cover the design and rationale:
|
|
|
106
106
|
6. [JSX runtime](./docs/6-jsx-runtime.md) — `SafeHtml`, `raw`, server-rendering.
|
|
107
107
|
7. [SVG handling](./docs/7-svg.md) — namespace propagation, `toElement`.
|
|
108
108
|
8. [API reference](./docs/8-api-reference.md) — every export, every option.
|
|
109
|
+
9. [Live demo](./docs/9-live-demo.md) — the GitHub Pages deploy of `examples/reactivity-demo`.
|
|
109
110
|
|
|
110
111
|
### For AI coding assistants
|
|
111
112
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { signal } from '@preact/signals-core';
|
|
2
|
+
export { batch, computed, effect, signal } from '@preact/signals-core';
|
|
3
|
+
|
|
4
|
+
// src/reactive.ts
|
|
5
|
+
|
|
6
|
+
// src/store.ts
|
|
7
|
+
var REGISTRY = [];
|
|
8
|
+
function defineStore(spec) {
|
|
9
|
+
const internal = signal(spec.initial());
|
|
10
|
+
const set = (next) => {
|
|
11
|
+
internal.value = next;
|
|
12
|
+
};
|
|
13
|
+
const get = () => internal.value;
|
|
14
|
+
const actions = spec.actions(set, get);
|
|
15
|
+
const store = {
|
|
16
|
+
state: internal,
|
|
17
|
+
actions,
|
|
18
|
+
reset() {
|
|
19
|
+
internal.value = spec.initial();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
REGISTRY.push(store);
|
|
23
|
+
return store;
|
|
24
|
+
}
|
|
25
|
+
function resetAllStores() {
|
|
26
|
+
for (const s of REGISTRY) s.reset();
|
|
27
|
+
}
|
|
28
|
+
function clearStoreRegistry() {
|
|
29
|
+
REGISTRY.length = 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { clearStoreRegistry, defineStore, resetAllStores };
|
|
33
|
+
//# sourceMappingURL=chunk-IZJIKRCE.js.map
|
|
34
|
+
//# sourceMappingURL=chunk-IZJIKRCE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store.ts"],"names":[],"mappings":";;;;;;AAqCA,IAAM,WAAyC,EAAC;AAEzC,SAAS,YACd,IAAA,EACyB;AACzB,EAAA,MAAM,QAAA,GAA2B,MAAA,CAAO,IAAA,CAAK,OAAA,EAAS,CAAA;AAEtD,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAAuB;AAClC,IAAA,QAAA,CAAS,KAAA,GAAQ,IAAA;AAAA,EACnB,CAAA;AACA,EAAA,MAAM,GAAA,GAAM,MAAc,QAAA,CAAS,KAAA;AAEnC,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAA;AAErC,EAAA,MAAM,KAAA,GAAiC;AAAA,IACrC,KAAA,EAAO,QAAA;AAAA,IACP,OAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,QAAA,CAAS,KAAA,GAAQ,KAAK,OAAA,EAAQ;AAAA,IAChC;AAAA,GACF;AAEA,EAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AACnB,EAAA,OAAO,KAAA;AACT;AAOO,SAAS,cAAA,GAAuB;AACrC,EAAA,KAAA,MAAW,CAAA,IAAK,QAAA,EAAU,CAAA,CAAE,KAAA,EAAM;AACpC;AAMO,SAAS,kBAAA,GAA2B;AACzC,EAAA,QAAA,CAAS,MAAA,GAAS,CAAA;AACpB","file":"chunk-IZJIKRCE.js","sourcesContent":["/**\n * `defineStore({ initial, actions })` — composable testable stores layered on\n * top of `reactive.ts`'s signals.\n *\n * Three rules:\n * 1. `state` is read-only. Consumers read via `state.value` or subscribe via\n * `effect()`. They cannot write directly.\n * 2. `actions` is the only mutation surface. All writes go through named\n * action functions. This is what makes stores testable — assert against\n * actions, not against arbitrary writes.\n * 3. `reset()` resets to `initial()`. Always defined; tests use it for\n * setup, lifecycle hooks (route change, sign-out, etc.) use it for\n * tear-down.\n *\n * A module-level registry tracks every store created via `defineStore()`;\n * `resetAllStores()` walks the registry and calls each `reset()`. Useful for\n * tests + project-switch / logout / route-reset scenarios where every piece\n * of client state should return to its initial shape.\n */\n\nimport type { ReadonlySignal, Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\nexport interface Store<TState, TActions> {\n /** Read-only reactive view. Consumers read `state.value` or subscribe via `effect()`. */\n readonly state: ReadonlySignal<TState>;\n /** Named mutators — the only way to change state. */\n readonly actions: TActions;\n /** Reset state to `initial()`. Used by tests and lifecycle hooks. */\n reset(): void;\n}\n\ninterface DefineStoreSpec<TState, TActions> {\n initial: () => TState;\n actions: (set: (next: TState) => void, get: () => TState) => TActions;\n}\n\nconst REGISTRY: Array<{ reset: () => void }> = [];\n\nexport function defineStore<TState, TActions>(\n spec: DefineStoreSpec<TState, TActions>,\n): Store<TState, TActions> {\n const internal: Signal<TState> = signal(spec.initial());\n\n const set = (next: TState): void => {\n internal.value = next;\n };\n const get = (): TState => internal.value;\n\n const actions = spec.actions(set, get);\n\n const store: Store<TState, TActions> = {\n state: internal,\n actions,\n reset() {\n internal.value = spec.initial();\n },\n };\n\n REGISTRY.push(store);\n return store;\n}\n\n/**\n * Reset every store registered via `defineStore()` to its `initial()` value.\n * Used by tests and by application lifecycle hooks (project switch, logout,\n * route reset).\n */\nexport function resetAllStores(): void {\n for (const s of REGISTRY) s.reset();\n}\n\n/**\n * Test helper — clears the registry. Exposed via the `kerfjs/testing` subpath,\n * not the main `kerfjs` entry. Unit tests use it to isolate stores between cases.\n */\nexport function clearStoreRegistry(): void {\n REGISTRY.length = 0;\n}\n"]}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// src/utils/escapeHtml.ts
|
|
2
|
+
function escapeHtml(str) {
|
|
3
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4
|
+
}
|
|
5
|
+
function escapeAttr(str) {
|
|
6
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/utils/jsx-attr-aliases.ts
|
|
10
|
+
var ATTR_ALIASES = {
|
|
11
|
+
// HTML attributes
|
|
12
|
+
className: "class",
|
|
13
|
+
htmlFor: "for",
|
|
14
|
+
httpEquiv: "http-equiv",
|
|
15
|
+
acceptCharset: "accept-charset",
|
|
16
|
+
accessKey: "accesskey",
|
|
17
|
+
autoCapitalize: "autocapitalize",
|
|
18
|
+
autoComplete: "autocomplete",
|
|
19
|
+
autoFocus: "autofocus",
|
|
20
|
+
autoPlay: "autoplay",
|
|
21
|
+
colSpan: "colspan",
|
|
22
|
+
contentEditable: "contenteditable",
|
|
23
|
+
crossOrigin: "crossorigin",
|
|
24
|
+
dateTime: "datetime",
|
|
25
|
+
defaultChecked: "checked",
|
|
26
|
+
defaultValue: "value",
|
|
27
|
+
encType: "enctype",
|
|
28
|
+
formAction: "formaction",
|
|
29
|
+
formEncType: "formenctype",
|
|
30
|
+
formMethod: "formmethod",
|
|
31
|
+
formNoValidate: "formnovalidate",
|
|
32
|
+
formTarget: "formtarget",
|
|
33
|
+
hrefLang: "hreflang",
|
|
34
|
+
inputMode: "inputmode",
|
|
35
|
+
maxLength: "maxlength",
|
|
36
|
+
minLength: "minlength",
|
|
37
|
+
noModule: "nomodule",
|
|
38
|
+
noValidate: "novalidate",
|
|
39
|
+
readOnly: "readonly",
|
|
40
|
+
referrerPolicy: "referrerpolicy",
|
|
41
|
+
rowSpan: "rowspan",
|
|
42
|
+
spellCheck: "spellcheck",
|
|
43
|
+
srcDoc: "srcdoc",
|
|
44
|
+
srcLang: "srclang",
|
|
45
|
+
srcSet: "srcset",
|
|
46
|
+
tabIndex: "tabindex",
|
|
47
|
+
useMap: "usemap",
|
|
48
|
+
// SVG presentation attributes (camelCase → kebab-case)
|
|
49
|
+
strokeWidth: "stroke-width",
|
|
50
|
+
strokeLinecap: "stroke-linecap",
|
|
51
|
+
strokeLinejoin: "stroke-linejoin",
|
|
52
|
+
strokeDasharray: "stroke-dasharray",
|
|
53
|
+
strokeDashoffset: "stroke-dashoffset",
|
|
54
|
+
strokeMiterlimit: "stroke-miterlimit",
|
|
55
|
+
strokeOpacity: "stroke-opacity",
|
|
56
|
+
fillOpacity: "fill-opacity",
|
|
57
|
+
fillRule: "fill-rule",
|
|
58
|
+
clipPath: "clip-path",
|
|
59
|
+
clipRule: "clip-rule",
|
|
60
|
+
colorInterpolation: "color-interpolation",
|
|
61
|
+
colorInterpolationFilters: "color-interpolation-filters",
|
|
62
|
+
floodColor: "flood-color",
|
|
63
|
+
floodOpacity: "flood-opacity",
|
|
64
|
+
lightingColor: "lighting-color",
|
|
65
|
+
stopColor: "stop-color",
|
|
66
|
+
stopOpacity: "stop-opacity",
|
|
67
|
+
shapeRendering: "shape-rendering",
|
|
68
|
+
imageRendering: "image-rendering",
|
|
69
|
+
textRendering: "text-rendering",
|
|
70
|
+
pointerEvents: "pointer-events",
|
|
71
|
+
vectorEffect: "vector-effect",
|
|
72
|
+
paintOrder: "paint-order",
|
|
73
|
+
// SVG text/font attributes
|
|
74
|
+
fontFamily: "font-family",
|
|
75
|
+
fontSize: "font-size",
|
|
76
|
+
fontStyle: "font-style",
|
|
77
|
+
fontVariant: "font-variant",
|
|
78
|
+
fontWeight: "font-weight",
|
|
79
|
+
fontStretch: "font-stretch",
|
|
80
|
+
textAnchor: "text-anchor",
|
|
81
|
+
textDecoration: "text-decoration",
|
|
82
|
+
dominantBaseline: "dominant-baseline",
|
|
83
|
+
alignmentBaseline: "alignment-baseline",
|
|
84
|
+
baselineShift: "baseline-shift",
|
|
85
|
+
letterSpacing: "letter-spacing",
|
|
86
|
+
wordSpacing: "word-spacing",
|
|
87
|
+
writingMode: "writing-mode",
|
|
88
|
+
// SVG marker attributes
|
|
89
|
+
markerStart: "marker-start",
|
|
90
|
+
markerMid: "marker-mid",
|
|
91
|
+
markerEnd: "marker-end",
|
|
92
|
+
// SVG xlink (legacy but still used)
|
|
93
|
+
xlinkHref: "xlink:href",
|
|
94
|
+
xlinkShow: "xlink:show",
|
|
95
|
+
xlinkActuate: "xlink:actuate",
|
|
96
|
+
xlinkType: "xlink:type",
|
|
97
|
+
xlinkRole: "xlink:role",
|
|
98
|
+
xlinkTitle: "xlink:title",
|
|
99
|
+
xlinkArcrole: "xlink:arcrole",
|
|
100
|
+
xmlBase: "xml:base",
|
|
101
|
+
xmlLang: "xml:lang",
|
|
102
|
+
xmlSpace: "xml:space",
|
|
103
|
+
xmlnsXlink: "xmlns:xlink"
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/jsx-runtime.ts
|
|
107
|
+
var SAFE_HTML_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
|
|
108
|
+
var SafeHtml = class {
|
|
109
|
+
__html;
|
|
110
|
+
// Branded so `isSafeHtml()` recognises instances from any copy of this module.
|
|
111
|
+
[SAFE_HTML_BRAND] = true;
|
|
112
|
+
constructor(html) {
|
|
113
|
+
this.__html = html;
|
|
114
|
+
}
|
|
115
|
+
toString() {
|
|
116
|
+
return this.__html;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function isSafeHtml(value) {
|
|
120
|
+
return typeof value === "object" && value !== null && value[SAFE_HTML_BRAND] === true;
|
|
121
|
+
}
|
|
122
|
+
function raw(html) {
|
|
123
|
+
return new SafeHtml(html);
|
|
124
|
+
}
|
|
125
|
+
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
126
|
+
"area",
|
|
127
|
+
"base",
|
|
128
|
+
"br",
|
|
129
|
+
"col",
|
|
130
|
+
"embed",
|
|
131
|
+
"hr",
|
|
132
|
+
"img",
|
|
133
|
+
"input",
|
|
134
|
+
"link",
|
|
135
|
+
"meta",
|
|
136
|
+
"source",
|
|
137
|
+
"track",
|
|
138
|
+
"wbr"
|
|
139
|
+
]);
|
|
140
|
+
function renderChildren(children) {
|
|
141
|
+
if (children == null || typeof children === "boolean") return "";
|
|
142
|
+
if (isSafeHtml(children)) return children.__html;
|
|
143
|
+
if (typeof children === "string") return escapeHtml(children);
|
|
144
|
+
if (typeof children === "number") return String(children);
|
|
145
|
+
if (Array.isArray(children)) return children.map(renderChildren).join("");
|
|
146
|
+
const maybeNode = children;
|
|
147
|
+
if (typeof maybeNode === "object" && maybeNode !== null && ("nodeType" in maybeNode || "outerHTML" in maybeNode)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
"JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). Build the tree in one JSX expression and use querySelector after toElement() to get element refs."
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
throw new Error(
|
|
153
|
+
`JSX: unsupported child of type ${describeValue(children)}. Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), passing a function (call it first), or passing a Promise (await it before render).`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
function describeValue(v) {
|
|
157
|
+
if (Array.isArray(v)) return "array";
|
|
158
|
+
if (typeof v === "object" && v !== null) {
|
|
159
|
+
const ctor = v.constructor?.name;
|
|
160
|
+
return ctor && ctor !== "Object" ? `object (${ctor})` : "object";
|
|
161
|
+
}
|
|
162
|
+
return typeof v;
|
|
163
|
+
}
|
|
164
|
+
function renderAttr(key, value) {
|
|
165
|
+
const name = ATTR_ALIASES[key] ?? key;
|
|
166
|
+
if (value == null || value === false) return "";
|
|
167
|
+
if (value === true) return ` ${name}`;
|
|
168
|
+
let strValue;
|
|
169
|
+
if (isSafeHtml(value)) {
|
|
170
|
+
strValue = value.__html;
|
|
171
|
+
} else if (typeof value === "number") {
|
|
172
|
+
strValue = String(value);
|
|
173
|
+
} else if (typeof value === "string") {
|
|
174
|
+
strValue = escapeAttr(value);
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`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?`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return ` ${name}="${strValue}"`;
|
|
181
|
+
}
|
|
182
|
+
function jsx(tag, props) {
|
|
183
|
+
if (typeof tag === "function") return tag(props);
|
|
184
|
+
const { children, ...attrs } = props;
|
|
185
|
+
const attrStr = Object.entries(attrs).map(([k, v]) => renderAttr(k, v)).join("");
|
|
186
|
+
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
|
|
187
|
+
const childStr = children != null ? renderChildren(children) : "";
|
|
188
|
+
return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);
|
|
189
|
+
}
|
|
190
|
+
function Fragment({ children }) {
|
|
191
|
+
return new SafeHtml(children != null ? renderChildren(children) : "");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { Fragment, SafeHtml, isSafeHtml, jsx, raw };
|
|
195
|
+
//# sourceMappingURL=chunk-URMYMSGU.js.map
|
|
196
|
+
//# sourceMappingURL=chunk-URMYMSGU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/escapeHtml.ts","../src/utils/jsx-attr-aliases.ts","../src/jsx-runtime.ts"],"names":[],"mappings":";AAMO,SAAS,WAAW,GAAA,EAAqB;AAC9C,EAAA,OAAO,GAAA,CACJ,OAAA,CAAQ,IAAA,EAAM,OAAO,EACrB,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,QAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,OAAA,CAAQ,MAAM,QAAQ,CAAA;AAC3B;AAEO,SAAS,WAAW,GAAA,EAAqB;AAC9C,EAAA,OAAO,IACJ,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA,CACrB,OAAA,CAAQ,MAAM,QAAQ,CAAA,CACtB,QAAQ,IAAA,EAAM,OAAO,EACrB,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,OAAA,CAAQ,MAAM,MAAM,CAAA;AACzB;;;ACTO,IAAM,YAAA,GAAuC;AAAA;AAAA,EAElD,SAAA,EAAW,OAAA;AAAA,EACX,OAAA,EAAS,KAAA;AAAA,EACT,SAAA,EAAW,YAAA;AAAA,EACX,aAAA,EAAe,gBAAA;AAAA,EACf,SAAA,EAAW,WAAA;AAAA,EACX,cAAA,EAAgB,gBAAA;AAAA,EAChB,YAAA,EAAc,cAAA;AAAA,EACd,SAAA,EAAW,WAAA;AAAA,EACX,QAAA,EAAU,UAAA;AAAA,EACV,OAAA,EAAS,SAAA;AAAA,EACT,eAAA,EAAiB,iBAAA;AAAA,EACjB,WAAA,EAAa,aAAA;AAAA,EACb,QAAA,EAAU,UAAA;AAAA,EACV,cAAA,EAAgB,SAAA;AAAA,EAChB,YAAA,EAAc,OAAA;AAAA,EACd,OAAA,EAAS,SAAA;AAAA,EACT,UAAA,EAAY,YAAA;AAAA,EACZ,WAAA,EAAa,aAAA;AAAA,EACb,UAAA,EAAY,YAAA;AAAA,EACZ,cAAA,EAAgB,gBAAA;AAAA,EAChB,UAAA,EAAY,YAAA;AAAA,EACZ,QAAA,EAAU,UAAA;AAAA,EACV,SAAA,EAAW,WAAA;AAAA,EACX,SAAA,EAAW,WAAA;AAAA,EACX,SAAA,EAAW,WAAA;AAAA,EACX,QAAA,EAAU,UAAA;AAAA,EACV,UAAA,EAAY,YAAA;AAAA,EACZ,QAAA,EAAU,UAAA;AAAA,EACV,cAAA,EAAgB,gBAAA;AAAA,EAChB,OAAA,EAAS,SAAA;AAAA,EACT,UAAA,EAAY,YAAA;AAAA,EACZ,MAAA,EAAQ,QAAA;AAAA,EACR,OAAA,EAAS,SAAA;AAAA,EACT,MAAA,EAAQ,QAAA;AAAA,EACR,QAAA,EAAU,UAAA;AAAA,EACV,MAAA,EAAQ,QAAA;AAAA;AAAA,EAGR,WAAA,EAAa,cAAA;AAAA,EACb,aAAA,EAAe,gBAAA;AAAA,EACf,cAAA,EAAgB,iBAAA;AAAA,EAChB,eAAA,EAAiB,kBAAA;AAAA,EACjB,gBAAA,EAAkB,mBAAA;AAAA,EAClB,gBAAA,EAAkB,mBAAA;AAAA,EAClB,aAAA,EAAe,gBAAA;AAAA,EACf,WAAA,EAAa,cAAA;AAAA,EACb,QAAA,EAAU,WAAA;AAAA,EACV,QAAA,EAAU,WAAA;AAAA,EACV,QAAA,EAAU,WAAA;AAAA,EACV,kBAAA,EAAoB,qBAAA;AAAA,EACpB,yBAAA,EAA2B,6BAAA;AAAA,EAC3B,UAAA,EAAY,aAAA;AAAA,EACZ,YAAA,EAAc,eAAA;AAAA,EACd,aAAA,EAAe,gBAAA;AAAA,EACf,SAAA,EAAW,YAAA;AAAA,EACX,WAAA,EAAa,cAAA;AAAA,EACb,cAAA,EAAgB,iBAAA;AAAA,EAChB,cAAA,EAAgB,iBAAA;AAAA,EAChB,aAAA,EAAe,gBAAA;AAAA,EACf,aAAA,EAAe,gBAAA;AAAA,EACf,YAAA,EAAc,eAAA;AAAA,EACd,UAAA,EAAY,aAAA;AAAA;AAAA,EAGZ,UAAA,EAAY,aAAA;AAAA,EACZ,QAAA,EAAU,WAAA;AAAA,EACV,SAAA,EAAW,YAAA;AAAA,EACX,WAAA,EAAa,cAAA;AAAA,EACb,UAAA,EAAY,aAAA;AAAA,EACZ,WAAA,EAAa,cAAA;AAAA,EACb,UAAA,EAAY,aAAA;AAAA,EACZ,cAAA,EAAgB,iBAAA;AAAA,EAChB,gBAAA,EAAkB,mBAAA;AAAA,EAClB,iBAAA,EAAmB,oBAAA;AAAA,EACnB,aAAA,EAAe,gBAAA;AAAA,EACf,aAAA,EAAe,gBAAA;AAAA,EACf,WAAA,EAAa,cAAA;AAAA,EACb,WAAA,EAAa,cAAA;AAAA;AAAA,EAGb,WAAA,EAAa,cAAA;AAAA,EACb,SAAA,EAAW,YAAA;AAAA,EACX,SAAA,EAAW,YAAA;AAAA;AAAA,EAGX,SAAA,EAAW,YAAA;AAAA,EACX,SAAA,EAAW,YAAA;AAAA,EACX,YAAA,EAAc,eAAA;AAAA,EACd,SAAA,EAAW,YAAA;AAAA,EACX,SAAA,EAAW,YAAA;AAAA,EACX,UAAA,EAAY,aAAA;AAAA,EACZ,YAAA,EAAc,eAAA;AAAA,EACd,OAAA,EAAS,UAAA;AAAA,EACT,OAAA,EAAS,UAAA;AAAA,EACT,QAAA,EAAU,WAAA;AAAA,EACV,UAAA,EAAY;AACd,CAAA;;;ACpFA,IAAM,eAAA,mBAAkB,MAAA,CAAO,GAAA,CAAI,iBAAiB,CAAA;AAE7C,IAAM,WAAN,MAAe;AAAA,EACX,MAAA;AAAA;AAAA,EAET,CAAU,eAAe,IAAI,IAAA;AAAA,EAC7B,YAAY,IAAA,EAAc;AACxB,IAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AAAA,EAChB;AAAA,EACA,QAAA,GAAmB;AACjB,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AACF;AAOO,SAAS,WAAW,KAAA,EAAmC;AAC5D,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IACnB,UAAU,IAAA,IACT,KAAA,CAAkC,eAAe,CAAA,KAAM,IAAA;AAC/D;AAGO,SAAS,IAAI,IAAA,EAAwB;AAC1C,EAAA,OAAO,IAAI,SAAS,IAAI,CAAA;AAC1B;AAUA,IAAM,SAAA,uBAAgB,GAAA,CAAI;AAAA,EACxB,MAAA;AAAA,EAAQ,MAAA;AAAA,EAAQ,IAAA;AAAA,EAAM,KAAA;AAAA,EAAO,OAAA;AAAA,EAAS,IAAA;AAAA,EAAM,KAAA;AAAA,EAAO,OAAA;AAAA,EACnD,MAAA;AAAA,EAAQ,MAAA;AAAA,EAAQ,QAAA;AAAA,EAAU,OAAA;AAAA,EAAS;AACrC,CAAC,CAAA;AAED,SAAS,eAAe,QAAA,EAA4B;AAClD,EAAA,IAAI,QAAA,IAAY,IAAA,IAAQ,OAAO,QAAA,KAAa,WAAW,OAAO,EAAA;AAC9D,EAAA,IAAI,UAAA,CAAW,QAAQ,CAAA,EAAG,OAAO,QAAA,CAAS,MAAA;AAC1C,EAAA,IAAI,OAAO,QAAA,KAAa,QAAA,EAAU,OAAO,WAAW,QAAQ,CAAA;AAC5D,EAAA,IAAI,OAAO,QAAA,KAAa,QAAA,EAAU,OAAO,OAAO,QAAQ,CAAA;AACxD,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAA,EAAG,OAAO,SAAS,GAAA,CAAI,cAAc,CAAA,CAAE,IAAA,CAAK,EAAE,CAAA;AAKxE,EAAA,MAAM,SAAA,GAAY,QAAA;AAClB,EAAA,IAAI,OAAO,cAAc,QAAA,IAAY,SAAA,KAAc,SAC3C,UAAA,IAAc,SAAA,IAAa,eAAe,SAAA,CAAA,EAAY;AAC5D,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AACA,EAAA,MAAM,IAAI,KAAA;AAAA,IACR,CAAA,+BAAA,EAAkC,aAAA,CAAc,QAAQ,CAAC,CAAA,gRAAA;AAAA,GAI3D;AACF;AAEA,SAAS,cAAc,CAAA,EAAoB;AACzC,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA,EAAG,OAAO,OAAA;AAC7B,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,IAAA,EAAM;AACvC,IAAA,MAAM,IAAA,GAAQ,EAA0C,WAAA,EAAa,IAAA;AACrE,IAAA,OAAO,IAAA,IAAQ,IAAA,KAAS,QAAA,GAAW,CAAA,QAAA,EAAW,IAAI,CAAA,CAAA,CAAA,GAAM,QAAA;AAAA,EAC1D;AACA,EAAA,OAAO,OAAO,CAAA;AAChB;AAEA,SAAS,UAAA,CAAW,KAAa,KAAA,EAAwB;AACvD,EAAA,MAAM,IAAA,GAAO,YAAA,CAAa,GAAG,CAAA,IAAK,GAAA;AAClC,EAAA,IAAI,KAAA,IAAS,IAAA,IAAQ,KAAA,KAAU,KAAA,EAAO,OAAO,EAAA;AAC7C,EAAA,IAAI,KAAA,KAAU,IAAA,EAAM,OAAO,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;AACnC,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI,UAAA,CAAW,KAAK,CAAA,EAAG;AACrB,IAAA,QAAA,GAAW,KAAA,CAAM,MAAA;AAAA,EACnB,CAAA,MAAA,IAAW,OAAO,KAAA,KAAU,QAAA,EAAU;AACpC,IAAA,QAAA,GAAW,OAAO,KAAK,CAAA;AAAA,EACzB,CAAA,MAAA,IAAW,OAAO,KAAA,KAAU,QAAA,EAAU;AACpC,IAAA,QAAA,GAAW,WAAW,KAAK,CAAA;AAAA,EAC7B,CAAA,MAAO;AACL,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,sCAAA,EAAyC,GAAG,CAAA,aAAA,EAAW,aAAA,CAAc,KAAK,CAAC,CAAA,0JAAA;AAAA,KAG7E;AAAA,EACF;AACA,EAAA,OAAO,CAAA,CAAA,EAAI,IAAI,CAAA,EAAA,EAAK,QAAQ,CAAA,CAAA,CAAA;AAC9B;AAEO,SAAS,GAAA,CAAI,KAA4C,KAAA,EAAwB;AACtF,EAAA,IAAI,OAAO,GAAA,KAAQ,UAAA,EAAY,OAAO,IAAI,KAAK,CAAA;AAE/C,EAAA,MAAM,EAAE,QAAA,EAAU,GAAG,KAAA,EAAM,GAAI,KAAA;AAC/B,EAAA,MAAM,UAAU,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,CACjC,IAAI,CAAC,CAAC,CAAA,EAAG,CAAC,MAAM,UAAA,CAAW,CAAA,EAAG,CAAC,CAAC,CAAA,CAChC,KAAK,EAAE,CAAA;AAEV,EAAA,IAAI,SAAA,CAAU,GAAA,CAAI,GAAG,CAAA,EAAG,OAAO,IAAI,QAAA,CAAS,CAAA,CAAA,EAAI,GAAG,CAAA,EAAG,OAAO,CAAA,CAAA,CAAG,CAAA;AAEhE,EAAA,MAAM,QAAA,GAAW,QAAA,IAAY,IAAA,GAAO,cAAA,CAAe,QAAQ,CAAA,GAAI,EAAA;AAC/D,EAAA,OAAO,IAAI,QAAA,CAAS,CAAA,CAAA,EAAI,GAAG,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,QAAQ,CAAA,EAAA,EAAK,GAAG,CAAA,CAAA,CAAG,CAAA;AAC9D;AAQO,SAAS,QAAA,CAAS,EAAE,QAAA,EAAS,EAAsC;AACxE,EAAA,OAAO,IAAI,QAAA,CAAS,QAAA,IAAY,OAAO,cAAA,CAAe,QAAQ,IAAI,EAAE,CAAA;AACtE","file":"chunk-URMYMSGU.js","sourcesContent":["/**\n * HTML / attribute escaping for the JSX runtime. Identical to the helpers\n * used in any reasonable HTML emitter — included here so kerf has no extra\n * runtime dependencies beyond `@preact/signals-core` and `morphdom`.\n */\n\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n}\n","/**\n * JSX → HTML / SVG attribute name aliases.\n *\n * The JSX runtime translates camelCase attributes (React convention) to\n * the kebab-case / colon-form names the browser actually wants. Anything\n * not in this map is passed through verbatim — `data-*`, `aria-*`, and\n * any custom attribute work without ceremony.\n *\n * Lives in its own module so `src/jsx-runtime.ts` can stay under the\n * 200-LOC project guideline; the bulk of `jsx-runtime.ts` was this table.\n */\n\nexport const ATTR_ALIASES: Record<string, string> = {\n // HTML attributes\n className: 'class',\n htmlFor: 'for',\n httpEquiv: 'http-equiv',\n acceptCharset: 'accept-charset',\n accessKey: 'accesskey',\n autoCapitalize: 'autocapitalize',\n autoComplete: 'autocomplete',\n autoFocus: 'autofocus',\n autoPlay: 'autoplay',\n colSpan: 'colspan',\n contentEditable: 'contenteditable',\n crossOrigin: 'crossorigin',\n dateTime: 'datetime',\n defaultChecked: 'checked',\n defaultValue: 'value',\n encType: 'enctype',\n formAction: 'formaction',\n formEncType: 'formenctype',\n formMethod: 'formmethod',\n formNoValidate: 'formnovalidate',\n formTarget: 'formtarget',\n hrefLang: 'hreflang',\n inputMode: 'inputmode',\n maxLength: 'maxlength',\n minLength: 'minlength',\n noModule: 'nomodule',\n noValidate: 'novalidate',\n readOnly: 'readonly',\n referrerPolicy: 'referrerpolicy',\n rowSpan: 'rowspan',\n spellCheck: 'spellcheck',\n srcDoc: 'srcdoc',\n srcLang: 'srclang',\n srcSet: 'srcset',\n tabIndex: 'tabindex',\n useMap: 'usemap',\n\n // SVG presentation attributes (camelCase → kebab-case)\n strokeWidth: 'stroke-width',\n strokeLinecap: 'stroke-linecap',\n strokeLinejoin: 'stroke-linejoin',\n strokeDasharray: 'stroke-dasharray',\n strokeDashoffset: 'stroke-dashoffset',\n strokeMiterlimit: 'stroke-miterlimit',\n strokeOpacity: 'stroke-opacity',\n fillOpacity: 'fill-opacity',\n fillRule: 'fill-rule',\n clipPath: 'clip-path',\n clipRule: 'clip-rule',\n colorInterpolation: 'color-interpolation',\n colorInterpolationFilters: 'color-interpolation-filters',\n floodColor: 'flood-color',\n floodOpacity: 'flood-opacity',\n lightingColor: 'lighting-color',\n stopColor: 'stop-color',\n stopOpacity: 'stop-opacity',\n shapeRendering: 'shape-rendering',\n imageRendering: 'image-rendering',\n textRendering: 'text-rendering',\n pointerEvents: 'pointer-events',\n vectorEffect: 'vector-effect',\n paintOrder: 'paint-order',\n\n // SVG text/font attributes\n fontFamily: 'font-family',\n fontSize: 'font-size',\n fontStyle: 'font-style',\n fontVariant: 'font-variant',\n fontWeight: 'font-weight',\n fontStretch: 'font-stretch',\n textAnchor: 'text-anchor',\n textDecoration: 'text-decoration',\n dominantBaseline: 'dominant-baseline',\n alignmentBaseline: 'alignment-baseline',\n baselineShift: 'baseline-shift',\n letterSpacing: 'letter-spacing',\n wordSpacing: 'word-spacing',\n writingMode: 'writing-mode',\n\n // SVG marker attributes\n markerStart: 'marker-start',\n markerMid: 'marker-mid',\n markerEnd: 'marker-end',\n\n // SVG xlink (legacy but still used)\n xlinkHref: 'xlink:href',\n xlinkShow: 'xlink:show',\n xlinkActuate: 'xlink:actuate',\n xlinkType: 'xlink:type',\n xlinkRole: 'xlink:role',\n xlinkTitle: 'xlink:title',\n xlinkArcrole: 'xlink:arcrole',\n xmlBase: 'xml:base',\n xmlLang: 'xml:lang',\n xmlSpace: 'xml:space',\n xmlnsXlink: 'xmlns:xlink',\n};\n","/**\n * kerf JSX runtime.\n *\n * JSX renders to `SafeHtml` — a wrapped HTML string. `SafeHtml.toString()`\n * is what the consumer eventually feeds into `mount()` (which morphs the\n * live DOM toward the new tree) or into `toElement()` (which parses it\n * to a single DOM node).\n *\n * Configure in your `tsconfig.json`:\n *\n * \"jsx\": \"react-jsx\",\n * \"jsxImportSource\": \"kerfjs\"\n *\n * Then write JSX as you normally would — kerf provides the `jsx` /\n * `jsxs` / `jsxDEV` / `Fragment` exports the JSX transform looks for.\n */\n\nimport { escapeAttr, escapeHtml } from './utils/escapeHtml.js';\nimport { ATTR_ALIASES } from './utils/jsx-attr-aliases.js';\n\n// Cross-realm/cross-bundle brand. Using `Symbol.for` (the global registry)\n// means two `SafeHtml` classes from different module copies still recognise\n// each other. Same approach React uses for `$$typeof: Symbol.for('react.element')`.\n// Without this, `instanceof SafeHtml` fails when the consumer's bundler ends\n// up loading two copies of kerf (separate barrel + jsx-runtime entries,\n// monorepo dedup misses, ESM/CJS interop, etc.).\nconst SAFE_HTML_BRAND = Symbol.for('kerfjs.SafeHtml');\n\nexport class SafeHtml {\n readonly __html: string;\n // Branded so `isSafeHtml()` recognises instances from any copy of this module.\n readonly [SAFE_HTML_BRAND] = true as const;\n constructor(html: string) {\n this.__html = html;\n }\n toString(): string {\n return this.__html;\n }\n}\n\n/**\n * Type guard for `SafeHtml`. Prefer this over `instanceof SafeHtml` — it works\n * across module copies (e.g. when the consumer's bundler loads kerf's barrel\n * and JSX-runtime entries as independent modules).\n */\nexport function isSafeHtml(value: unknown): value is SafeHtml {\n return typeof value === 'object'\n && value !== null\n && (value as Record<symbol, unknown>)[SAFE_HTML_BRAND] === true;\n}\n\n/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */\nexport function raw(html: string): SafeHtml {\n return new SafeHtml(html);\n}\n\ntype Child = SafeHtml | string | number | boolean | null | undefined;\ntype Children = Child | Children[];\n\ninterface Props {\n children?: Children;\n [key: string]: unknown;\n}\n\nconst VOID_TAGS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'source', 'track', 'wbr',\n]);\n\nfunction renderChildren(children: Children): string {\n if (children == null || typeof children === 'boolean') return '';\n if (isSafeHtml(children)) return children.__html;\n if (typeof children === 'string') return escapeHtml(children);\n if (typeof children === 'number') return String(children);\n if (Array.isArray(children)) return children.map(renderChildren).join('');\n // Catch the common mistake of passing a DOM element (e.g. the result of\n // toElement(...)) as a JSX child. The runtime renders to HTML strings, so\n // DOM nodes can't be composed — they'd silently serialize to \"\" and their\n // event listeners would be lost. Throw loudly so this can't sneak in.\n const maybeNode = children as unknown;\n if (typeof maybeNode === 'object' && maybeNode !== null\n && ('nodeType' in maybeNode || 'outerHTML' in maybeNode)) {\n throw new Error(\n 'JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). '\n + 'Build the tree in one JSX expression and use querySelector after toElement() to get element refs.',\n );\n }\n throw new Error(\n `JSX: unsupported child of type ${describeValue(children)}. `\n + 'Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. '\n + 'Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), '\n + 'passing a function (call it first), or passing a Promise (await it before render).',\n );\n}\n\nfunction describeValue(v: unknown): string {\n if (Array.isArray(v)) return 'array';\n if (typeof v === 'object' && v !== null) {\n const ctor = (v as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== 'Object' ? `object (${ctor})` : 'object';\n }\n return typeof v;\n}\n\nfunction renderAttr(key: string, value: unknown): string {\n const name = ATTR_ALIASES[key] ?? key;\n if (value == null || value === false) return '';\n if (value === true) return ` ${name}`;\n let strValue: string;\n if (isSafeHtml(value)) {\n strValue = value.__html;\n } else if (typeof value === 'number') {\n strValue = String(value);\n } else if (typeof value === 'string') {\n strValue = escapeAttr(value);\n } else {\n throw new Error(\n `JSX: unsupported value for attribute \"${key}\" — got ${describeValue(value)}. `\n + 'Attribute values must be string, number, boolean, null, undefined, or SafeHtml. '\n + 'Did you mean to read .value off a Signal, or stringify the object first?',\n );\n }\n return ` ${name}=\"${strValue}\"`;\n}\n\nexport function jsx(tag: string | ((props: Props) => SafeHtml), props: Props): SafeHtml {\n if (typeof tag === 'function') return tag(props);\n\n const { children, ...attrs } = props;\n const attrStr = Object.entries(attrs)\n .map(([k, v]) => renderAttr(k, v))\n .join('');\n\n if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);\n\n const childStr = children != null ? renderChildren(children) : '';\n return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);\n}\n\nexport { jsx as jsxs };\n// vitest's dev-mode JSX transform emits `jsxDEV(tag, props, ...)`; the\n// alias lets tests import this module without the production build pipeline\n// caring.\nexport { jsx as jsxDEV };\n\nexport function Fragment({ children }: { children?: Children }): SafeHtml {\n return new SafeHtml(children != null ? renderChildren(children) : '');\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace JSX {\n export type Element = SafeHtml;\n export interface ElementChildrenAttribute {\n children: unknown;\n }\n export interface IntrinsicElements {\n [elemName: string]: Record<string, unknown>;\n }\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SafeHtml } from './jsx-runtime.js';
|
|
2
|
-
export { raw } from './jsx-runtime.js';
|
|
2
|
+
export { isSafeHtml, raw } from './jsx-runtime.js';
|
|
3
3
|
export { ReadonlySignal, Signal, batch, computed, effect, signal } from '@preact/signals-core';
|
|
4
4
|
export { S as Store, d as defineStore, r as resetAllStores } from './testing-CdMgVVoI.js';
|
|
5
5
|
|
|
@@ -11,13 +11,14 @@ export { S as Store, d as defineStore, r as resetAllStores } from './testing-CdM
|
|
|
11
11
|
* Three-tier listener model:
|
|
12
12
|
*
|
|
13
13
|
* - Tier 1 (bubbling events) — use `delegate()`.
|
|
14
|
-
* click, input, change, submit, keydown/
|
|
15
|
-
* contextmenu, wheel, copy/paste/cut.
|
|
14
|
+
* click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,
|
|
15
|
+
* drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.
|
|
16
16
|
*
|
|
17
|
-
* - Tier 2 (non-bubbling events
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* - Tier 2 (non-bubbling events) — use `delegateCapture()`.
|
|
18
|
+
* focus, blur, scroll, load, error, mouseenter, mouseleave.
|
|
19
|
+
* The capture phase fires on the way down from the root to the target,
|
|
20
|
+
* so a root-level listener with `capture: true` reaches events that
|
|
21
|
+
* wouldn't bubble back up.
|
|
21
22
|
*
|
|
22
23
|
* - Tier 3 (per-element instances / library-owned subtrees) — mark the
|
|
23
24
|
* host element with `data-morph-skip` and manage the library's
|
|
@@ -75,9 +76,14 @@ declare function delegateCapture(rootEl: HTMLElement, type: string, selector: st
|
|
|
75
76
|
* inside on subsequent renders. Used for library-owned subtrees (xterm-
|
|
76
77
|
* style widgets, charts, third-party editors) where the library's own
|
|
77
78
|
* lifecycle manages the children.
|
|
78
|
-
* - Focused text-entry inputs (`<input>` of typing kinds, `<textarea
|
|
79
|
-
*
|
|
80
|
-
*
|
|
79
|
+
* - Focused text-entry inputs (`<input>` of typing kinds, `<textarea>`)
|
|
80
|
+
* keep their current value + selection range across morphs while focused.
|
|
81
|
+
* The user never sees their cursor jump mid-keystroke.
|
|
82
|
+
* - Focused `[contenteditable]` elements have their entire subtree
|
|
83
|
+
* skipped (same mechanism as `data-morph-skip`). The user's in-progress
|
|
84
|
+
* edit — typed content, caret position, multi-range selections, anything
|
|
85
|
+
* else they did to the DOM — survives verbatim. The next render after
|
|
86
|
+
* blur catches up.
|
|
81
87
|
*/
|
|
82
88
|
declare function mount(rootEl: HTMLElement, render: () => SafeHtml | string): () => void;
|
|
83
89
|
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { isSafeHtml } from './chunk-URMYMSGU.js';
|
|
2
|
+
export { SafeHtml, isSafeHtml, raw } from './chunk-URMYMSGU.js';
|
|
3
|
+
import { effect } from './chunk-IZJIKRCE.js';
|
|
4
|
+
export { batch, computed, defineStore, effect, resetAllStores, signal } from './chunk-IZJIKRCE.js';
|
|
1
5
|
import morphdom from 'morphdom';
|
|
2
|
-
import { effect, signal } from '@preact/signals-core';
|
|
3
|
-
export { batch, computed, effect, signal } from '@preact/signals-core';
|
|
4
6
|
|
|
5
7
|
// src/delegate.ts
|
|
6
8
|
function assertValidSelector(selector, fn) {
|
|
@@ -41,26 +43,12 @@ function delegateCapture(rootEl, type, selector, handler) {
|
|
|
41
43
|
rootEl.removeEventListener(type, listener, true);
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
var SafeHtml = class {
|
|
47
|
-
__html;
|
|
48
|
-
constructor(html) {
|
|
49
|
-
this.__html = html;
|
|
50
|
-
}
|
|
51
|
-
toString() {
|
|
52
|
-
return this.__html;
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
function raw(html) {
|
|
56
|
-
return new SafeHtml(html);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/mount.ts
|
|
46
|
+
var ID_KEY_PREFIX = "id:";
|
|
47
|
+
var DATA_KEY_PREFIX = "data-key:";
|
|
60
48
|
function mount(rootEl, render) {
|
|
61
49
|
return effect(() => {
|
|
62
50
|
const next = render();
|
|
63
|
-
const html = next
|
|
51
|
+
const html = isSafeHtml(next) ? next.toString() : next;
|
|
64
52
|
const template = rootEl.cloneNode(false);
|
|
65
53
|
template.innerHTML = html;
|
|
66
54
|
morphdom(rootEl, template, {
|
|
@@ -68,28 +56,30 @@ function mount(rootEl, render) {
|
|
|
68
56
|
getNodeKey: (node) => {
|
|
69
57
|
if (node.nodeType !== 1) return void 0;
|
|
70
58
|
const el = node;
|
|
71
|
-
if (el.id !== "") return el.id
|
|
72
|
-
if (el.dataset.key != null) return
|
|
59
|
+
if (el.id !== "") return `${ID_KEY_PREFIX}${el.id}`;
|
|
60
|
+
if (el.dataset.key != null) return `${DATA_KEY_PREFIX}${el.dataset.key}`;
|
|
73
61
|
return void 0;
|
|
74
62
|
},
|
|
75
63
|
onBeforeElUpdated: (fromEl, toEl) => {
|
|
76
64
|
if (fromEl.dataset.morphSkip != null) return false;
|
|
77
65
|
if (fromEl.isEqualNode(toEl)) return false;
|
|
78
|
-
if (fromEl === document.activeElement
|
|
79
|
-
|
|
66
|
+
if (fromEl === document.activeElement) {
|
|
67
|
+
const ce = fromEl.getAttribute("contenteditable");
|
|
68
|
+
if (ce !== null && ce.toLowerCase() !== "false") return false;
|
|
69
|
+
if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
|
|
80
70
|
}
|
|
81
71
|
return true;
|
|
82
72
|
}
|
|
83
73
|
});
|
|
84
74
|
});
|
|
85
75
|
}
|
|
86
|
-
function
|
|
76
|
+
function isTextInputOrTextarea(el) {
|
|
87
77
|
if (el.tagName === "TEXTAREA") return true;
|
|
88
78
|
if (el.tagName === "INPUT") {
|
|
89
79
|
const type = el.type;
|
|
90
80
|
return type === "text" || type === "search" || type === "url" || type === "email" || type === "tel" || type === "password" || type === "";
|
|
91
81
|
}
|
|
92
|
-
return
|
|
82
|
+
return false;
|
|
93
83
|
}
|
|
94
84
|
function preserveTextEntryState(fromEl, toEl) {
|
|
95
85
|
if (fromEl.tagName === "TEXTAREA" || fromEl.tagName === "INPUT") {
|
|
@@ -103,29 +93,6 @@ function preserveTextEntryState(fromEl, toEl) {
|
|
|
103
93
|
}
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
// src/store.ts
|
|
107
|
-
var REGISTRY = [];
|
|
108
|
-
function defineStore(spec) {
|
|
109
|
-
const internal = signal(spec.initial());
|
|
110
|
-
const set = (next) => {
|
|
111
|
-
internal.value = next;
|
|
112
|
-
};
|
|
113
|
-
const get = () => internal.value;
|
|
114
|
-
const actions = spec.actions(set, get);
|
|
115
|
-
const store = {
|
|
116
|
-
state: internal,
|
|
117
|
-
actions,
|
|
118
|
-
reset() {
|
|
119
|
-
internal.value = spec.initial();
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
REGISTRY.push(store);
|
|
123
|
-
return store;
|
|
124
|
-
}
|
|
125
|
-
function resetAllStores() {
|
|
126
|
-
for (const s of REGISTRY) s.reset();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
96
|
// src/toElement.ts
|
|
130
97
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
131
98
|
var SVG_FRAGMENT_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -153,34 +120,33 @@ var SVG_FRAGMENT_TAGS = /* @__PURE__ */ new Set([
|
|
|
153
120
|
"image",
|
|
154
121
|
"foreignObject"
|
|
155
122
|
]);
|
|
123
|
+
var EXCERPT_MAX_LEN = 100;
|
|
156
124
|
function leadingTag(html) {
|
|
157
125
|
const match = /^\s*<([a-zA-Z][a-zA-Z0-9]*)\b/.exec(html);
|
|
158
126
|
return match !== null ? match[1] : null;
|
|
159
127
|
}
|
|
160
128
|
function excerpt(html) {
|
|
161
129
|
const trimmed = html.trim();
|
|
162
|
-
return trimmed.length >
|
|
130
|
+
return trimmed.length > EXCERPT_MAX_LEN ? `${trimmed.slice(0, EXCERPT_MAX_LEN)}\u2026` : trimmed;
|
|
131
|
+
}
|
|
132
|
+
function parseSvgOrThrow(html, label, originalHtml) {
|
|
133
|
+
const doc = new DOMParser().parseFromString(html, "image/svg+xml");
|
|
134
|
+
const err = doc.querySelector("parsererror");
|
|
135
|
+
if (err !== null) {
|
|
136
|
+
throw new Error(`toElement: ${label} parse error \u2014 ${err.textContent}
|
|
137
|
+
input: ${excerpt(originalHtml)}`);
|
|
138
|
+
}
|
|
139
|
+
return doc;
|
|
163
140
|
}
|
|
164
141
|
function toElement(jsx) {
|
|
165
142
|
const html = typeof jsx === "string" ? jsx : jsx.toString();
|
|
166
143
|
const tag = leadingTag(html);
|
|
167
144
|
if (tag === "svg") {
|
|
168
|
-
|
|
169
|
-
const err = doc.querySelector("parsererror");
|
|
170
|
-
if (err !== null) {
|
|
171
|
-
throw new Error(`toElement: SVG parse error \u2014 ${err.textContent}
|
|
172
|
-
input: ${excerpt(html)}`);
|
|
173
|
-
}
|
|
174
|
-
return doc.documentElement;
|
|
145
|
+
return parseSvgOrThrow(html, "SVG", html).documentElement;
|
|
175
146
|
}
|
|
176
147
|
if (tag !== null && SVG_FRAGMENT_TAGS.has(tag)) {
|
|
177
148
|
const wrapped = `<svg xmlns="${SVG_NS}">${html}</svg>`;
|
|
178
|
-
const doc =
|
|
179
|
-
const err = doc.querySelector("parsererror");
|
|
180
|
-
if (err !== null) {
|
|
181
|
-
throw new Error(`toElement: SVG fragment parse error \u2014 ${err.textContent}
|
|
182
|
-
input: ${excerpt(html)}`);
|
|
183
|
-
}
|
|
149
|
+
const doc = parseSvgOrThrow(wrapped, "SVG fragment", html);
|
|
184
150
|
const first = doc.documentElement.firstElementChild;
|
|
185
151
|
if (first === null) throw new Error(`toElement: SVG fragment produced no element
|
|
186
152
|
input: ${excerpt(html)}`);
|
|
@@ -194,6 +160,6 @@ function toElement(jsx) {
|
|
|
194
160
|
return child;
|
|
195
161
|
}
|
|
196
162
|
|
|
197
|
-
export {
|
|
163
|
+
export { delegate, delegateCapture, mount, toElement };
|
|
198
164
|
//# sourceMappingURL=index.js.map
|
|
199
165
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/delegate.ts","../src/jsx-runtime.ts","../src/mount.ts","../src/store.ts","../src/toElement.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Tiny event-delegation helpers. Replace per-element `addEventListener` calls\n * (which don't survive morph re-renders for nodes morphdom creates) with one\n * listener at the morph-root that dispatches via `closest()`.\n *\n * Three-tier listener model:\n *\n * - Tier 1 (bubbling events) — use `delegate()`.\n * click, input, change, submit, keydown/keyup, pointer*, drag*, drop,\n * contextmenu, wheel, copy/paste/cut.\n *\n * - Tier 2 (non-bubbling events: focus / blur / scroll / load / error) —\n * use `delegateCapture()`. The capture phase fires on the way down from\n * the root to the target, so a root-level listener with `capture: true`\n * reaches events that wouldn't bubble back up.\n *\n * - Tier 3 (per-element instances / library-owned subtrees) — mark the\n * host element with `data-morph-skip` and manage the library's\n * lifecycle directly. No delegation helper applies.\n */\n\ntype Handler = (event: Event, target: Element) => void;\n\n/**\n * Validate a CSS selector at registration time, so a typo throws immediately\n * with the bad selector quoted instead of producing a cryptic DOMException\n * the first time a matching event fires.\n */\nfunction assertValidSelector(selector: string, fn: string): void {\n try {\n document.createElement('div').matches(selector);\n } catch {\n throw new Error(\n `${fn}: invalid selector \"${selector}\". `\n + 'Pass a valid CSS selector (e.g. \\'[data-action=\"add\"]\\', \\'.btn\\', \\'input\\').',\n );\n }\n}\n\n/**\n * Bubble-phase delegation. Installs ONE listener on `rootEl` for the given\n * event type. When the event fires, walks up from `event.target` to the root\n * looking for an element matching `selector`; if found, fires `handler` with\n * the matched element as the second arg.\n *\n * Returns a disposer that removes the listener.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegate(rootEl, 'click', '[data-action=\"add\"]', handlerFn);\n */\nexport function delegate(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: Handler,\n): () => void {\n assertValidSelector(selector, 'delegate');\n const listener = (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const matched = target.closest(selector);\n if (matched !== null && rootEl.contains(matched)) {\n handler(event, matched);\n }\n };\n rootEl.addEventListener(type, listener);\n return () => {\n rootEl.removeEventListener(type, listener);\n };\n}\n\n/**\n * Capture-phase delegation — for non-bubbling events (`focus`, `blur`,\n * `scroll`, `load`, `error`). Reaches descendants of `rootEl` that match\n * `selector` regardless of how many times morphdom has rebuilt them.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);\n */\nexport function delegateCapture(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: Handler,\n): () => void {\n assertValidSelector(selector, 'delegateCapture');\n const listener = (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n if (target.matches(selector) && rootEl.contains(target)) {\n handler(event, target);\n }\n };\n rootEl.addEventListener(type, listener, true);\n return () => {\n rootEl.removeEventListener(type, listener, true);\n };\n}\n","/**\n * kerf JSX runtime.\n *\n * JSX renders to `SafeHtml` — a wrapped HTML string. `SafeHtml.toString()`\n * is what the consumer eventually feeds into `mount()` (which morphs the\n * live DOM toward the new tree) or into `toElement()` (which parses it\n * to a single DOM node).\n *\n * Configure in your `tsconfig.json`:\n *\n * \"jsx\": \"react-jsx\",\n * \"jsxImportSource\": \"kerfjs\"\n *\n * Then write JSX as you normally would — kerf provides the `jsx` /\n * `jsxs` / `jsxDEV` / `Fragment` exports the JSX transform looks for.\n */\n\nimport { escapeAttr, escapeHtml } from './utils/escapeHtml.js';\n\nexport class SafeHtml {\n readonly __html: string;\n constructor(html: string) {\n this.__html = html;\n }\n toString(): string {\n return this.__html;\n }\n}\n\n/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */\nexport function raw(html: string): SafeHtml {\n return new SafeHtml(html);\n}\n\ntype Child = SafeHtml | string | number | boolean | null | undefined;\ntype Children = Child | Children[];\n\ninterface Props {\n children?: Children;\n [key: string]: unknown;\n}\n\nconst VOID_TAGS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'source', 'track', 'wbr',\n]);\n\nfunction renderChildren(children: Children): string {\n if (children == null || typeof children === 'boolean') return '';\n if (children instanceof SafeHtml) return children.__html;\n if (typeof children === 'string') return escapeHtml(children);\n if (typeof children === 'number') return String(children);\n if (Array.isArray(children)) return children.map(renderChildren).join('');\n // Catch the common mistake of passing a DOM element (e.g. the result of\n // toElement(...)) as a JSX child. The runtime renders to HTML strings, so\n // DOM nodes can't be composed — they'd silently serialize to \"\" and their\n // event listeners would be lost. Throw loudly so this can't sneak in.\n const maybeNode = children as unknown;\n if (typeof maybeNode === 'object' && maybeNode !== null\n && ('nodeType' in maybeNode || 'outerHTML' in maybeNode)) {\n throw new Error(\n 'JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). '\n + 'Build the tree in one JSX expression and use querySelector after toElement() to get element refs.',\n );\n }\n throw new Error(\n `JSX: unsupported child of type ${describeValue(children)}. `\n + 'Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. '\n + 'Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), '\n + 'passing a function (call it first), or passing a Promise (await it before render).',\n );\n}\n\nfunction describeValue(v: unknown): string {\n if (Array.isArray(v)) return 'array';\n if (typeof v === 'object' && v !== null) {\n const ctor = (v as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== 'Object' ? `object (${ctor})` : 'object';\n }\n return typeof v;\n}\n\nconst ATTR_ALIASES: Record<string, string> = {\n // HTML attributes\n className: 'class',\n htmlFor: 'for',\n httpEquiv: 'http-equiv',\n acceptCharset: 'accept-charset',\n accessKey: 'accesskey',\n autoCapitalize: 'autocapitalize',\n autoComplete: 'autocomplete',\n autoFocus: 'autofocus',\n autoPlay: 'autoplay',\n colSpan: 'colspan',\n contentEditable: 'contenteditable',\n crossOrigin: 'crossorigin',\n dateTime: 'datetime',\n defaultChecked: 'checked',\n defaultValue: 'value',\n encType: 'enctype',\n formAction: 'formaction',\n formEncType: 'formenctype',\n formMethod: 'formmethod',\n formNoValidate: 'formnovalidate',\n formTarget: 'formtarget',\n hrefLang: 'hreflang',\n inputMode: 'inputmode',\n maxLength: 'maxlength',\n minLength: 'minlength',\n noModule: 'nomodule',\n noValidate: 'novalidate',\n readOnly: 'readonly',\n referrerPolicy: 'referrerpolicy',\n rowSpan: 'rowspan',\n spellCheck: 'spellcheck',\n srcDoc: 'srcdoc',\n srcLang: 'srclang',\n srcSet: 'srcset',\n tabIndex: 'tabindex',\n useMap: 'usemap',\n\n // SVG presentation attributes (camelCase → kebab-case)\n strokeWidth: 'stroke-width',\n strokeLinecap: 'stroke-linecap',\n strokeLinejoin: 'stroke-linejoin',\n strokeDasharray: 'stroke-dasharray',\n strokeDashoffset: 'stroke-dashoffset',\n strokeMiterlimit: 'stroke-miterlimit',\n strokeOpacity: 'stroke-opacity',\n fillOpacity: 'fill-opacity',\n fillRule: 'fill-rule',\n clipPath: 'clip-path',\n clipRule: 'clip-rule',\n colorInterpolation: 'color-interpolation',\n colorInterpolationFilters: 'color-interpolation-filters',\n floodColor: 'flood-color',\n floodOpacity: 'flood-opacity',\n lightingColor: 'lighting-color',\n stopColor: 'stop-color',\n stopOpacity: 'stop-opacity',\n shapeRendering: 'shape-rendering',\n imageRendering: 'image-rendering',\n textRendering: 'text-rendering',\n pointerEvents: 'pointer-events',\n vectorEffect: 'vector-effect',\n paintOrder: 'paint-order',\n\n // SVG text/font attributes\n fontFamily: 'font-family',\n fontSize: 'font-size',\n fontStyle: 'font-style',\n fontVariant: 'font-variant',\n fontWeight: 'font-weight',\n fontStretch: 'font-stretch',\n textAnchor: 'text-anchor',\n textDecoration: 'text-decoration',\n dominantBaseline: 'dominant-baseline',\n alignmentBaseline: 'alignment-baseline',\n baselineShift: 'baseline-shift',\n letterSpacing: 'letter-spacing',\n wordSpacing: 'word-spacing',\n writingMode: 'writing-mode',\n\n // SVG marker attributes\n markerStart: 'marker-start',\n markerMid: 'marker-mid',\n markerEnd: 'marker-end',\n\n // SVG xlink (legacy but still used)\n xlinkHref: 'xlink:href',\n xlinkShow: 'xlink:show',\n xlinkActuate: 'xlink:actuate',\n xlinkType: 'xlink:type',\n xlinkRole: 'xlink:role',\n xlinkTitle: 'xlink:title',\n xlinkArcrole: 'xlink:arcrole',\n xmlBase: 'xml:base',\n xmlLang: 'xml:lang',\n xmlSpace: 'xml:space',\n xmlnsXlink: 'xmlns:xlink',\n};\n\nfunction renderAttr(key: string, value: unknown): string {\n const name = ATTR_ALIASES[key] ?? key;\n if (value == null || value === false) return '';\n if (value === true) return ` ${name}`;\n let strValue: string;\n if (value instanceof SafeHtml) {\n strValue = value.__html;\n } else if (typeof value === 'number') {\n strValue = String(value);\n } else if (typeof value === 'string') {\n strValue = escapeAttr(value);\n } else {\n throw new Error(\n `JSX: unsupported value for attribute \"${key}\" — got ${describeValue(value)}. `\n + 'Attribute values must be string, number, boolean, null, undefined, or SafeHtml. '\n + 'Did you mean to read .value off a Signal, or stringify the object first?',\n );\n }\n return ` ${name}=\"${strValue}\"`;\n}\n\nexport function jsx(tag: string | ((props: Props) => SafeHtml), props: Props): SafeHtml {\n if (typeof tag === 'function') return tag(props);\n\n const { children, ...attrs } = props;\n const attrStr = Object.entries(attrs)\n .map(([k, v]) => renderAttr(k, v))\n .join('');\n\n if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);\n\n const childStr = children != null ? renderChildren(children) : '';\n return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);\n}\n\nexport { jsx as jsxs };\n// vitest's dev-mode JSX transform emits `jsxDEV(tag, props, ...)`; the\n// alias lets tests import this module without the production build pipeline\n// caring.\nexport { jsx as jsxDEV };\n\nexport function Fragment({ children }: { children?: Children }): SafeHtml {\n return new SafeHtml(children != null ? renderChildren(children) : '');\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace JSX {\n export type Element = SafeHtml;\n export interface ElementChildrenAttribute {\n children: unknown;\n }\n export interface IntrinsicElements {\n [elemName: string]: Record<string, unknown>;\n }\n}\n","/**\n * `mount(rootEl, render)` — kerf's render primitive.\n *\n * Wraps `effect()` from `reactive.ts` so that whenever any signal read inside\n * `render()` changes, we re-run `render()` and use `morphdom` to apply the\n * minimal set of DOM mutations against the live tree. Element identity (and\n * thus focus, selection, in-flight pointer interactions, and event listeners\n * on preserved nodes) is preserved wherever the keyed/positional diff matches.\n *\n * Compared to a `replaceChildren(...rows.map(toElement))` rebuild pattern, the\n * user-visible win is that an `<input>` the user is typing into survives an\n * unrelated re-render — its DOM node, focus state, and cursor position are\n * not destroyed and recreated on each tick.\n */\n\nimport morphdom from 'morphdom';\n\nimport { SafeHtml } from './jsx-runtime.js';\nimport { effect } from './reactive.js';\n\n/**\n * Bind `render()` to the children of `rootEl`. Re-runs whenever any signal\n * read inside `render()` changes. Returns a disposer that tears down the\n * effect; call it when the host element is removed from the DOM.\n *\n * Conventions:\n *\n * - Diff keys: `id` and `data-key` are matched across the morph by key\n * rather than positionally, so list reorders move existing nodes instead\n * of churning unrelated siblings.\n * - `data-morph-skip`: any element with this attribute is left untouched\n * inside on subsequent renders. Used for library-owned subtrees (xterm-\n * style widgets, charts, third-party editors) where the library's own\n * lifecycle manages the children.\n * - Focused text-entry inputs (`<input>` of typing kinds, `<textarea>`,\n * `[contenteditable]`) keep their current value + selection range across\n * morphs while focused. The user never sees their cursor jump mid-keystroke.\n */\nexport function mount(rootEl: HTMLElement, render: () => SafeHtml | string): () => void {\n return effect(() => {\n const next = render();\n const html = next instanceof SafeHtml ? next.toString() : next;\n\n const template = rootEl.cloneNode(false) as HTMLElement;\n template.innerHTML = html;\n\n morphdom(rootEl, template, {\n childrenOnly: true,\n getNodeKey: (node) => {\n if (node.nodeType !== 1) return undefined;\n const el = node as HTMLElement;\n if (el.id !== '') return el.id;\n if (el.dataset.key != null) return `key:${el.dataset.key}`;\n return undefined;\n },\n onBeforeElUpdated: (fromEl, toEl) => {\n if (fromEl.dataset.morphSkip != null) return false;\n if (fromEl.isEqualNode(toEl)) return false;\n if (fromEl === document.activeElement && isTextEntry(fromEl)) {\n preserveTextEntryState(fromEl, toEl);\n }\n return true;\n },\n });\n });\n}\n\nfunction isTextEntry(el: Element): boolean {\n if (el.tagName === 'TEXTAREA') return true;\n if (el.tagName === 'INPUT') {\n const type = (el as HTMLInputElement).type;\n return type === 'text' || type === 'search' || type === 'url' || type === 'email'\n || type === 'tel' || type === 'password' || type === '';\n }\n return (el as HTMLElement).isContentEditable;\n}\n\nfunction preserveTextEntryState(fromEl: HTMLElement, toEl: HTMLElement): void {\n if (fromEl.tagName === 'TEXTAREA' || fromEl.tagName === 'INPUT') {\n const fromInput = fromEl as HTMLInputElement;\n const toInput = toEl as HTMLInputElement;\n toInput.value = fromInput.value;\n try {\n toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd);\n } catch {\n // Some input types (number, range, color, …) reject selection APIs.\n }\n }\n}\n","/**\n * `defineStore({ initial, actions })` — composable testable stores layered on\n * top of `reactive.ts`'s signals.\n *\n * Three rules:\n * 1. `state` is read-only. Consumers read via `state.value` or subscribe via\n * `effect()`. They cannot write directly.\n * 2. `actions` is the only mutation surface. All writes go through named\n * action functions. This is what makes stores testable — assert against\n * actions, not against arbitrary writes.\n * 3. `reset()` resets to `initial()`. Always defined; tests use it for\n * setup, lifecycle hooks (route change, sign-out, etc.) use it for\n * tear-down.\n *\n * A module-level registry tracks every store created via `defineStore()`;\n * `resetAllStores()` walks the registry and calls each `reset()`. Useful for\n * tests + project-switch / logout / route-reset scenarios where every piece\n * of client state should return to its initial shape.\n */\n\nimport type { ReadonlySignal, Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\nexport interface Store<TState, TActions> {\n /** Read-only reactive view. Consumers read `state.value` or subscribe via `effect()`. */\n readonly state: ReadonlySignal<TState>;\n /** Named mutators — the only way to change state. */\n readonly actions: TActions;\n /** Reset state to `initial()`. Used by tests and lifecycle hooks. */\n reset(): void;\n}\n\ninterface DefineStoreSpec<TState, TActions> {\n initial: () => TState;\n actions: (set: (next: TState) => void, get: () => TState) => TActions;\n}\n\nconst REGISTRY: Array<{ reset: () => void }> = [];\n\nexport function defineStore<TState, TActions>(\n spec: DefineStoreSpec<TState, TActions>,\n): Store<TState, TActions> {\n const internal: Signal<TState> = signal(spec.initial());\n\n const set = (next: TState): void => {\n internal.value = next;\n };\n const get = (): TState => internal.value;\n\n const actions = spec.actions(set, get);\n\n const store: Store<TState, TActions> = {\n state: internal,\n actions,\n reset() {\n internal.value = spec.initial();\n },\n };\n\n REGISTRY.push(store);\n return store;\n}\n\n/**\n * Reset every store registered via `defineStore()` to its `initial()` value.\n * Used by tests and by application lifecycle hooks (project switch, logout,\n * route reset).\n */\nexport function resetAllStores(): void {\n for (const s of REGISTRY) s.reset();\n}\n\n/**\n * Test helper — clears the registry. Exposed via the `kerfjs/testing` subpath,\n * not the main `kerfjs` entry. Unit tests use it to isolate stores between cases.\n */\nexport function clearStoreRegistry(): void {\n REGISTRY.length = 0;\n}\n","/**\n * `toElement(jsx)` — JSX → DOM, with SVG-aware namespace handling.\n *\n * The naive implementation parses JSX through a `<template>` element's\n * `innerHTML`. That works for HTML and for SVG fragments whose root tag is\n * `<svg>` (the parser switches to \"foreign content\" mode). It silently\n * fails for SVG fragments WITHOUT an `<svg>` wrapper — descendants come out\n * as `HTMLUnknownElement` and never paint.\n *\n * `toElement` detects SVG content and routes through `DOMParser` with the\n * `image/svg+xml` MIME, which guarantees correct namespacing for all\n * descendants. HTML content takes the original `<template>` path unchanged.\n */\n\nimport type { SafeHtml } from './jsx-runtime.js';\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\nconst SVG_FRAGMENT_TAGS = new Set([\n 'g', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse',\n 'text', 'tspan', 'defs', 'use', 'symbol', 'clipPath', 'mask', 'pattern',\n 'filter', 'marker', 'linearGradient', 'radialGradient', 'stop', 'image',\n 'foreignObject',\n]);\n\nfunction leadingTag(html: string): string | null {\n const match = /^\\s*<([a-zA-Z][a-zA-Z0-9]*)\\b/.exec(html);\n return match !== null ? match[1] : null;\n}\n\nfunction excerpt(html: string): string {\n const trimmed = html.trim();\n return trimmed.length > 100 ? `${trimmed.slice(0, 100)}…` : trimmed;\n}\n\nexport function toElement(jsx: SafeHtml | string): Element {\n const html = typeof jsx === 'string' ? jsx : jsx.toString();\n const tag = leadingTag(html);\n\n if (tag === 'svg') {\n // SVG root — parse as XML to guarantee namespace propagation.\n const doc = new DOMParser().parseFromString(html, 'image/svg+xml');\n const err = doc.querySelector('parsererror');\n if (err !== null) {\n throw new Error(`toElement: SVG parse error — ${err.textContent}\\n input: ${excerpt(html)}`);\n }\n return doc.documentElement;\n }\n\n if (tag !== null && SVG_FRAGMENT_TAGS.has(tag)) {\n // SVG fragment without an <svg> wrapper — wrap, parse, unwrap.\n const wrapped = `<svg xmlns=\"${SVG_NS}\">${html}</svg>`;\n const doc = new DOMParser().parseFromString(wrapped, 'image/svg+xml');\n const err = doc.querySelector('parsererror');\n if (err !== null) {\n throw new Error(`toElement: SVG fragment parse error — ${err.textContent}\\n input: ${excerpt(html)}`);\n }\n const first = doc.documentElement.firstElementChild;\n /* c8 ignore next 2 — defensive: a successful XML parse of a wrapped svg always yields ≥1 child. */\n if (first === null) throw new Error(`toElement: SVG fragment produced no element\\n input: ${excerpt(html)}`);\n return first;\n }\n\n // HTML — `<template>`-based parse.\n const t = document.createElement('template');\n t.innerHTML = html;\n const child = t.content.firstElementChild;\n if (child === null) throw new Error(`toElement: produced no element\\n input: ${excerpt(html)}`);\n return child;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/delegate.ts","../src/mount.ts","../src/toElement.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Tiny event-delegation helpers. Replace per-element `addEventListener` calls\n * (which don't survive morph re-renders for nodes morphdom creates) with one\n * listener at the morph-root that dispatches via `closest()`.\n *\n * Three-tier listener model:\n *\n * - Tier 1 (bubbling events) — use `delegate()`.\n * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,\n * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.\n *\n * - Tier 2 (non-bubbling events) — use `delegateCapture()`.\n * focus, blur, scroll, load, error, mouseenter, mouseleave.\n * The capture phase fires on the way down from the root to the target,\n * so a root-level listener with `capture: true` reaches events that\n * wouldn't bubble back up.\n *\n * - Tier 3 (per-element instances / library-owned subtrees) — mark the\n * host element with `data-morph-skip` and manage the library's\n * lifecycle directly. No delegation helper applies.\n */\n\ntype Handler = (event: Event, target: Element) => void;\n\n/**\n * Validate a CSS selector at registration time, so a typo throws immediately\n * with the bad selector quoted instead of producing a cryptic DOMException\n * the first time a matching event fires.\n */\nfunction assertValidSelector(selector: string, fn: string): void {\n try {\n document.createElement('div').matches(selector);\n } catch {\n throw new Error(\n `${fn}: invalid selector \"${selector}\". `\n + 'Pass a valid CSS selector (e.g. \\'[data-action=\"add\"]\\', \\'.btn\\', \\'input\\').',\n );\n }\n}\n\n/**\n * Bubble-phase delegation. Installs ONE listener on `rootEl` for the given\n * event type. When the event fires, walks up from `event.target` to the root\n * looking for an element matching `selector`; if found, fires `handler` with\n * the matched element as the second arg.\n *\n * Returns a disposer that removes the listener.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegate(rootEl, 'click', '[data-action=\"add\"]', handlerFn);\n */\nexport function delegate(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: Handler,\n): () => void {\n assertValidSelector(selector, 'delegate');\n const listener = (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const matched = target.closest(selector);\n if (matched !== null && rootEl.contains(matched)) {\n handler(event, matched);\n }\n };\n rootEl.addEventListener(type, listener);\n return () => {\n rootEl.removeEventListener(type, listener);\n };\n}\n\n/**\n * Capture-phase delegation — for non-bubbling events (`focus`, `blur`,\n * `scroll`, `load`, `error`). Reaches descendants of `rootEl` that match\n * `selector` regardless of how many times morphdom has rebuilt them.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);\n */\nexport function delegateCapture(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: Handler,\n): () => void {\n assertValidSelector(selector, 'delegateCapture');\n const listener = (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n if (target.matches(selector) && rootEl.contains(target)) {\n handler(event, target);\n }\n };\n rootEl.addEventListener(type, listener, true);\n return () => {\n rootEl.removeEventListener(type, listener, true);\n };\n}\n","/**\n * `mount(rootEl, render)` — kerf's render primitive.\n *\n * Wraps `effect()` from `reactive.ts` so that whenever any signal read inside\n * `render()` changes, we re-run `render()` and use `morphdom` to apply the\n * minimal set of DOM mutations against the live tree. Element identity (and\n * thus focus, selection, in-flight pointer interactions, and event listeners\n * on preserved nodes) is preserved wherever the keyed/positional diff matches.\n *\n * Compared to a `replaceChildren(...rows.map(toElement))` rebuild pattern, the\n * user-visible win is that an `<input>` the user is typing into survives an\n * unrelated re-render — its DOM node, focus state, and cursor position are\n * not destroyed and recreated on each tick.\n */\n\nimport morphdom from 'morphdom';\n\nimport type { SafeHtml } from './jsx-runtime.js';\nimport { isSafeHtml } from './jsx-runtime.js';\nimport { effect } from './reactive.js';\n\n// Distinct namespaces inside morphdom's flat string-keyed match space, so a\n// consumer with `id=\"foo\"` and a sibling with `data-key=\"foo\"` cannot collide.\n// The prefixes also can't collide with each other across consumer values\n// (e.g. `id=\"data-key:foo\"` vs `data-key=\"foo\"` would have produced the same\n// key under a single-prefix scheme; here they don't).\nconst ID_KEY_PREFIX = 'id:';\nconst DATA_KEY_PREFIX = 'data-key:';\n\n/**\n * Bind `render()` to the children of `rootEl`. Re-runs whenever any signal\n * read inside `render()` changes. Returns a disposer that tears down the\n * effect; call it when the host element is removed from the DOM.\n *\n * Conventions:\n *\n * - Diff keys: `id` and `data-key` are matched across the morph by key\n * rather than positionally, so list reorders move existing nodes instead\n * of churning unrelated siblings.\n * - `data-morph-skip`: any element with this attribute is left untouched\n * inside on subsequent renders. Used for library-owned subtrees (xterm-\n * style widgets, charts, third-party editors) where the library's own\n * lifecycle manages the children.\n * - Focused text-entry inputs (`<input>` of typing kinds, `<textarea>`)\n * keep their current value + selection range across morphs while focused.\n * The user never sees their cursor jump mid-keystroke.\n * - Focused `[contenteditable]` elements have their entire subtree\n * skipped (same mechanism as `data-morph-skip`). The user's in-progress\n * edit — typed content, caret position, multi-range selections, anything\n * else they did to the DOM — survives verbatim. The next render after\n * blur catches up.\n */\nexport function mount(rootEl: HTMLElement, render: () => SafeHtml | string): () => void {\n return effect(() => {\n const next = render();\n const html = isSafeHtml(next) ? next.toString() : next;\n\n const template = rootEl.cloneNode(false) as HTMLElement;\n template.innerHTML = html;\n\n morphdom(rootEl, template, {\n childrenOnly: true,\n getNodeKey: (node) => {\n if (node.nodeType !== 1) return undefined;\n const el = node as HTMLElement;\n if (el.id !== '') return `${ID_KEY_PREFIX}${el.id}`;\n if (el.dataset.key != null) return `${DATA_KEY_PREFIX}${el.dataset.key}`;\n return undefined;\n },\n onBeforeElUpdated: (fromEl, toEl) => {\n if (fromEl.dataset.morphSkip != null) return false;\n if (fromEl.isEqualNode(toEl)) return false;\n if (fromEl === document.activeElement) {\n // Focused contenteditable: skip the entire subtree so the user's\n // in-progress edit (typed content + caret + multi-range selection)\n // is not disturbed. A subsequent render — typically after blur or\n // an explicit signal write — catches up. We read the attribute\n // directly rather than the derived `isContentEditable` property\n // because happy-dom (test environment) doesn't always populate\n // the latter; the attribute is the spec's source of truth either\n // way. Per the HTML spec, any value other than `\"false\"` (case-\n // insensitive), including the empty string, means editable.\n const ce = fromEl.getAttribute('contenteditable');\n if (ce !== null && ce.toLowerCase() !== 'false') return false;\n // Focused INPUT / TEXTAREA: copy live value + selection onto the\n // morph target before letting morphdom proceed.\n if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);\n }\n return true;\n },\n });\n });\n}\n\nfunction isTextInputOrTextarea(el: Element): boolean {\n if (el.tagName === 'TEXTAREA') return true;\n if (el.tagName === 'INPUT') {\n const type = (el as HTMLInputElement).type;\n return type === 'text' || type === 'search' || type === 'url' || type === 'email'\n || type === 'tel' || type === 'password' || type === '';\n }\n return false;\n}\n\nfunction preserveTextEntryState(fromEl: HTMLElement, toEl: HTMLElement): void {\n if (fromEl.tagName === 'TEXTAREA' || fromEl.tagName === 'INPUT') {\n const fromInput = fromEl as HTMLInputElement;\n const toInput = toEl as HTMLInputElement;\n toInput.value = fromInput.value;\n try {\n toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd);\n } catch {\n // Some input types (number, range, color, …) reject selection APIs.\n }\n }\n}\n","/**\n * `toElement(jsx)` — JSX → DOM, with SVG-aware namespace handling.\n *\n * The naive implementation parses JSX through a `<template>` element's\n * `innerHTML`. That works for HTML and for SVG fragments whose root tag is\n * `<svg>` (the parser switches to \"foreign content\" mode). It silently\n * fails for SVG fragments WITHOUT an `<svg>` wrapper — descendants come out\n * as `HTMLUnknownElement` and never paint.\n *\n * `toElement` detects SVG content and routes through `DOMParser` with the\n * `image/svg+xml` MIME, which guarantees correct namespacing for all\n * descendants. HTML content takes the original `<template>` path unchanged.\n */\n\nimport type { SafeHtml } from './jsx-runtime.js';\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\nconst SVG_FRAGMENT_TAGS = new Set([\n 'g', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse',\n 'text', 'tspan', 'defs', 'use', 'symbol', 'clipPath', 'mask', 'pattern',\n 'filter', 'marker', 'linearGradient', 'radialGradient', 'stop', 'image',\n 'foreignObject',\n]);\n\nconst EXCERPT_MAX_LEN = 100;\n\nfunction leadingTag(html: string): string | null {\n const match = /^\\s*<([a-zA-Z][a-zA-Z0-9]*)\\b/.exec(html);\n return match !== null ? match[1] : null;\n}\n\nfunction excerpt(html: string): string {\n const trimmed = html.trim();\n return trimmed.length > EXCERPT_MAX_LEN ? `${trimmed.slice(0, EXCERPT_MAX_LEN)}…` : trimmed;\n}\n\nfunction parseSvgOrThrow(html: string, label: string, originalHtml: string): Document {\n const doc = new DOMParser().parseFromString(html, 'image/svg+xml');\n const err = doc.querySelector('parsererror');\n if (err !== null) {\n throw new Error(`toElement: ${label} parse error — ${err.textContent}\\n input: ${excerpt(originalHtml)}`);\n }\n return doc;\n}\n\nexport function toElement(jsx: SafeHtml | string): Element {\n const html = typeof jsx === 'string' ? jsx : jsx.toString();\n const tag = leadingTag(html);\n\n if (tag === 'svg') {\n // SVG root — parse as XML to guarantee namespace propagation.\n return parseSvgOrThrow(html, 'SVG', html).documentElement;\n }\n\n if (tag !== null && SVG_FRAGMENT_TAGS.has(tag)) {\n // SVG fragment without an <svg> wrapper — wrap, parse, unwrap.\n const wrapped = `<svg xmlns=\"${SVG_NS}\">${html}</svg>`;\n const doc = parseSvgOrThrow(wrapped, 'SVG fragment', html);\n const first = doc.documentElement.firstElementChild;\n /* c8 ignore next 2 — defensive: a successful XML parse of a wrapped svg always yields ≥1 child. */\n if (first === null) throw new Error(`toElement: SVG fragment produced no element\\n input: ${excerpt(html)}`);\n return first;\n }\n\n // HTML — `<template>`-based parse.\n const t = document.createElement('template');\n t.innerHTML = html;\n const child = t.content.firstElementChild;\n if (child === null) throw new Error(`toElement: produced no element\\n input: ${excerpt(html)}`);\n return child;\n}\n"]}
|
package/dist/jsx-runtime.d.ts
CHANGED
|
@@ -14,11 +14,19 @@
|
|
|
14
14
|
* Then write JSX as you normally would — kerf provides the `jsx` /
|
|
15
15
|
* `jsxs` / `jsxDEV` / `Fragment` exports the JSX transform looks for.
|
|
16
16
|
*/
|
|
17
|
+
declare const SAFE_HTML_BRAND: unique symbol;
|
|
17
18
|
declare class SafeHtml {
|
|
18
19
|
readonly __html: string;
|
|
20
|
+
readonly [SAFE_HTML_BRAND]: true;
|
|
19
21
|
constructor(html: string);
|
|
20
22
|
toString(): string;
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Type guard for `SafeHtml`. Prefer this over `instanceof SafeHtml` — it works
|
|
26
|
+
* across module copies (e.g. when the consumer's bundler loads kerf's barrel
|
|
27
|
+
* and JSX-runtime entries as independent modules).
|
|
28
|
+
*/
|
|
29
|
+
declare function isSafeHtml(value: unknown): value is SafeHtml;
|
|
22
30
|
/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */
|
|
23
31
|
declare function raw(html: string): SafeHtml;
|
|
24
32
|
type Child = SafeHtml | string | number | boolean | null | undefined;
|
|
@@ -42,4 +50,4 @@ declare namespace JSX {
|
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
export { Fragment, JSX, SafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, raw };
|
|
53
|
+
export { Fragment, JSX, SafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, raw };
|
package/dist/jsx-runtime.js
CHANGED
|
@@ -1,188 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
function escapeHtml(str) {
|
|
3
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4
|
-
}
|
|
5
|
-
function escapeAttr(str) {
|
|
6
|
-
return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// src/jsx-runtime.ts
|
|
10
|
-
var SafeHtml = class {
|
|
11
|
-
__html;
|
|
12
|
-
constructor(html) {
|
|
13
|
-
this.__html = html;
|
|
14
|
-
}
|
|
15
|
-
toString() {
|
|
16
|
-
return this.__html;
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
function raw(html) {
|
|
20
|
-
return new SafeHtml(html);
|
|
21
|
-
}
|
|
22
|
-
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
23
|
-
"area",
|
|
24
|
-
"base",
|
|
25
|
-
"br",
|
|
26
|
-
"col",
|
|
27
|
-
"embed",
|
|
28
|
-
"hr",
|
|
29
|
-
"img",
|
|
30
|
-
"input",
|
|
31
|
-
"link",
|
|
32
|
-
"meta",
|
|
33
|
-
"source",
|
|
34
|
-
"track",
|
|
35
|
-
"wbr"
|
|
36
|
-
]);
|
|
37
|
-
function renderChildren(children) {
|
|
38
|
-
if (children == null || typeof children === "boolean") return "";
|
|
39
|
-
if (children instanceof SafeHtml) return children.__html;
|
|
40
|
-
if (typeof children === "string") return escapeHtml(children);
|
|
41
|
-
if (typeof children === "number") return String(children);
|
|
42
|
-
if (Array.isArray(children)) return children.map(renderChildren).join("");
|
|
43
|
-
const maybeNode = children;
|
|
44
|
-
if (typeof maybeNode === "object" && maybeNode !== null && ("nodeType" in maybeNode || "outerHTML" in maybeNode)) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
"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."
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
throw new Error(
|
|
50
|
-
`JSX: unsupported child of type ${describeValue(children)}. 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).`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
function describeValue(v) {
|
|
54
|
-
if (Array.isArray(v)) return "array";
|
|
55
|
-
if (typeof v === "object" && v !== null) {
|
|
56
|
-
const ctor = v.constructor?.name;
|
|
57
|
-
return ctor && ctor !== "Object" ? `object (${ctor})` : "object";
|
|
58
|
-
}
|
|
59
|
-
return typeof v;
|
|
60
|
-
}
|
|
61
|
-
var ATTR_ALIASES = {
|
|
62
|
-
// HTML attributes
|
|
63
|
-
className: "class",
|
|
64
|
-
htmlFor: "for",
|
|
65
|
-
httpEquiv: "http-equiv",
|
|
66
|
-
acceptCharset: "accept-charset",
|
|
67
|
-
accessKey: "accesskey",
|
|
68
|
-
autoCapitalize: "autocapitalize",
|
|
69
|
-
autoComplete: "autocomplete",
|
|
70
|
-
autoFocus: "autofocus",
|
|
71
|
-
autoPlay: "autoplay",
|
|
72
|
-
colSpan: "colspan",
|
|
73
|
-
contentEditable: "contenteditable",
|
|
74
|
-
crossOrigin: "crossorigin",
|
|
75
|
-
dateTime: "datetime",
|
|
76
|
-
defaultChecked: "checked",
|
|
77
|
-
defaultValue: "value",
|
|
78
|
-
encType: "enctype",
|
|
79
|
-
formAction: "formaction",
|
|
80
|
-
formEncType: "formenctype",
|
|
81
|
-
formMethod: "formmethod",
|
|
82
|
-
formNoValidate: "formnovalidate",
|
|
83
|
-
formTarget: "formtarget",
|
|
84
|
-
hrefLang: "hreflang",
|
|
85
|
-
inputMode: "inputmode",
|
|
86
|
-
maxLength: "maxlength",
|
|
87
|
-
minLength: "minlength",
|
|
88
|
-
noModule: "nomodule",
|
|
89
|
-
noValidate: "novalidate",
|
|
90
|
-
readOnly: "readonly",
|
|
91
|
-
referrerPolicy: "referrerpolicy",
|
|
92
|
-
rowSpan: "rowspan",
|
|
93
|
-
spellCheck: "spellcheck",
|
|
94
|
-
srcDoc: "srcdoc",
|
|
95
|
-
srcLang: "srclang",
|
|
96
|
-
srcSet: "srcset",
|
|
97
|
-
tabIndex: "tabindex",
|
|
98
|
-
useMap: "usemap",
|
|
99
|
-
// SVG presentation attributes (camelCase → kebab-case)
|
|
100
|
-
strokeWidth: "stroke-width",
|
|
101
|
-
strokeLinecap: "stroke-linecap",
|
|
102
|
-
strokeLinejoin: "stroke-linejoin",
|
|
103
|
-
strokeDasharray: "stroke-dasharray",
|
|
104
|
-
strokeDashoffset: "stroke-dashoffset",
|
|
105
|
-
strokeMiterlimit: "stroke-miterlimit",
|
|
106
|
-
strokeOpacity: "stroke-opacity",
|
|
107
|
-
fillOpacity: "fill-opacity",
|
|
108
|
-
fillRule: "fill-rule",
|
|
109
|
-
clipPath: "clip-path",
|
|
110
|
-
clipRule: "clip-rule",
|
|
111
|
-
colorInterpolation: "color-interpolation",
|
|
112
|
-
colorInterpolationFilters: "color-interpolation-filters",
|
|
113
|
-
floodColor: "flood-color",
|
|
114
|
-
floodOpacity: "flood-opacity",
|
|
115
|
-
lightingColor: "lighting-color",
|
|
116
|
-
stopColor: "stop-color",
|
|
117
|
-
stopOpacity: "stop-opacity",
|
|
118
|
-
shapeRendering: "shape-rendering",
|
|
119
|
-
imageRendering: "image-rendering",
|
|
120
|
-
textRendering: "text-rendering",
|
|
121
|
-
pointerEvents: "pointer-events",
|
|
122
|
-
vectorEffect: "vector-effect",
|
|
123
|
-
paintOrder: "paint-order",
|
|
124
|
-
// SVG text/font attributes
|
|
125
|
-
fontFamily: "font-family",
|
|
126
|
-
fontSize: "font-size",
|
|
127
|
-
fontStyle: "font-style",
|
|
128
|
-
fontVariant: "font-variant",
|
|
129
|
-
fontWeight: "font-weight",
|
|
130
|
-
fontStretch: "font-stretch",
|
|
131
|
-
textAnchor: "text-anchor",
|
|
132
|
-
textDecoration: "text-decoration",
|
|
133
|
-
dominantBaseline: "dominant-baseline",
|
|
134
|
-
alignmentBaseline: "alignment-baseline",
|
|
135
|
-
baselineShift: "baseline-shift",
|
|
136
|
-
letterSpacing: "letter-spacing",
|
|
137
|
-
wordSpacing: "word-spacing",
|
|
138
|
-
writingMode: "writing-mode",
|
|
139
|
-
// SVG marker attributes
|
|
140
|
-
markerStart: "marker-start",
|
|
141
|
-
markerMid: "marker-mid",
|
|
142
|
-
markerEnd: "marker-end",
|
|
143
|
-
// SVG xlink (legacy but still used)
|
|
144
|
-
xlinkHref: "xlink:href",
|
|
145
|
-
xlinkShow: "xlink:show",
|
|
146
|
-
xlinkActuate: "xlink:actuate",
|
|
147
|
-
xlinkType: "xlink:type",
|
|
148
|
-
xlinkRole: "xlink:role",
|
|
149
|
-
xlinkTitle: "xlink:title",
|
|
150
|
-
xlinkArcrole: "xlink:arcrole",
|
|
151
|
-
xmlBase: "xml:base",
|
|
152
|
-
xmlLang: "xml:lang",
|
|
153
|
-
xmlSpace: "xml:space",
|
|
154
|
-
xmlnsXlink: "xmlns:xlink"
|
|
155
|
-
};
|
|
156
|
-
function renderAttr(key, value) {
|
|
157
|
-
const name = ATTR_ALIASES[key] ?? key;
|
|
158
|
-
if (value == null || value === false) return "";
|
|
159
|
-
if (value === true) return ` ${name}`;
|
|
160
|
-
let strValue;
|
|
161
|
-
if (value instanceof SafeHtml) {
|
|
162
|
-
strValue = value.__html;
|
|
163
|
-
} else if (typeof value === "number") {
|
|
164
|
-
strValue = String(value);
|
|
165
|
-
} else if (typeof value === "string") {
|
|
166
|
-
strValue = escapeAttr(value);
|
|
167
|
-
} else {
|
|
168
|
-
throw new Error(
|
|
169
|
-
`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?`
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
return ` ${name}="${strValue}"`;
|
|
173
|
-
}
|
|
174
|
-
function jsx(tag, props) {
|
|
175
|
-
if (typeof tag === "function") return tag(props);
|
|
176
|
-
const { children, ...attrs } = props;
|
|
177
|
-
const attrStr = Object.entries(attrs).map(([k, v]) => renderAttr(k, v)).join("");
|
|
178
|
-
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
|
|
179
|
-
const childStr = children != null ? renderChildren(children) : "";
|
|
180
|
-
return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);
|
|
181
|
-
}
|
|
182
|
-
function Fragment({ children }) {
|
|
183
|
-
return new SafeHtml(children != null ? renderChildren(children) : "");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export { Fragment, SafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, raw };
|
|
1
|
+
export { Fragment, SafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, raw } from './chunk-URMYMSGU.js';
|
|
187
2
|
//# sourceMappingURL=jsx-runtime.js.map
|
|
188
3
|
//# sourceMappingURL=jsx-runtime.js.map
|
package/dist/jsx-runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/escapeHtml.ts","../src/jsx-runtime.ts"],"names":[],"mappings":"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","file":"jsx-runtime.js","sourcesContent":["/**\n * HTML / attribute escaping for the JSX runtime. Identical to the helpers\n * used in any reasonable HTML emitter — included here so kerf has no extra\n * runtime dependencies beyond `@preact/signals-core` and `morphdom`.\n */\n\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n}\n","/**\n * kerf JSX runtime.\n *\n * JSX renders to `SafeHtml` — a wrapped HTML string. `SafeHtml.toString()`\n * is what the consumer eventually feeds into `mount()` (which morphs the\n * live DOM toward the new tree) or into `toElement()` (which parses it\n * to a single DOM node).\n *\n * Configure in your `tsconfig.json`:\n *\n * \"jsx\": \"react-jsx\",\n * \"jsxImportSource\": \"kerfjs\"\n *\n * Then write JSX as you normally would — kerf provides the `jsx` /\n * `jsxs` / `jsxDEV` / `Fragment` exports the JSX transform looks for.\n */\n\nimport { escapeAttr, escapeHtml } from './utils/escapeHtml.js';\n\nexport class SafeHtml {\n readonly __html: string;\n constructor(html: string) {\n this.__html = html;\n }\n toString(): string {\n return this.__html;\n }\n}\n\n/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */\nexport function raw(html: string): SafeHtml {\n return new SafeHtml(html);\n}\n\ntype Child = SafeHtml | string | number | boolean | null | undefined;\ntype Children = Child | Children[];\n\ninterface Props {\n children?: Children;\n [key: string]: unknown;\n}\n\nconst VOID_TAGS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'source', 'track', 'wbr',\n]);\n\nfunction renderChildren(children: Children): string {\n if (children == null || typeof children === 'boolean') return '';\n if (children instanceof SafeHtml) return children.__html;\n if (typeof children === 'string') return escapeHtml(children);\n if (typeof children === 'number') return String(children);\n if (Array.isArray(children)) return children.map(renderChildren).join('');\n // Catch the common mistake of passing a DOM element (e.g. the result of\n // toElement(...)) as a JSX child. The runtime renders to HTML strings, so\n // DOM nodes can't be composed — they'd silently serialize to \"\" and their\n // event listeners would be lost. Throw loudly so this can't sneak in.\n const maybeNode = children as unknown;\n if (typeof maybeNode === 'object' && maybeNode !== null\n && ('nodeType' in maybeNode || 'outerHTML' in maybeNode)) {\n throw new Error(\n 'JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). '\n + 'Build the tree in one JSX expression and use querySelector after toElement() to get element refs.',\n );\n }\n throw new Error(\n `JSX: unsupported child of type ${describeValue(children)}. `\n + 'Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. '\n + 'Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), '\n + 'passing a function (call it first), or passing a Promise (await it before render).',\n );\n}\n\nfunction describeValue(v: unknown): string {\n if (Array.isArray(v)) return 'array';\n if (typeof v === 'object' && v !== null) {\n const ctor = (v as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== 'Object' ? `object (${ctor})` : 'object';\n }\n return typeof v;\n}\n\nconst ATTR_ALIASES: Record<string, string> = {\n // HTML attributes\n className: 'class',\n htmlFor: 'for',\n httpEquiv: 'http-equiv',\n acceptCharset: 'accept-charset',\n accessKey: 'accesskey',\n autoCapitalize: 'autocapitalize',\n autoComplete: 'autocomplete',\n autoFocus: 'autofocus',\n autoPlay: 'autoplay',\n colSpan: 'colspan',\n contentEditable: 'contenteditable',\n crossOrigin: 'crossorigin',\n dateTime: 'datetime',\n defaultChecked: 'checked',\n defaultValue: 'value',\n encType: 'enctype',\n formAction: 'formaction',\n formEncType: 'formenctype',\n formMethod: 'formmethod',\n formNoValidate: 'formnovalidate',\n formTarget: 'formtarget',\n hrefLang: 'hreflang',\n inputMode: 'inputmode',\n maxLength: 'maxlength',\n minLength: 'minlength',\n noModule: 'nomodule',\n noValidate: 'novalidate',\n readOnly: 'readonly',\n referrerPolicy: 'referrerpolicy',\n rowSpan: 'rowspan',\n spellCheck: 'spellcheck',\n srcDoc: 'srcdoc',\n srcLang: 'srclang',\n srcSet: 'srcset',\n tabIndex: 'tabindex',\n useMap: 'usemap',\n\n // SVG presentation attributes (camelCase → kebab-case)\n strokeWidth: 'stroke-width',\n strokeLinecap: 'stroke-linecap',\n strokeLinejoin: 'stroke-linejoin',\n strokeDasharray: 'stroke-dasharray',\n strokeDashoffset: 'stroke-dashoffset',\n strokeMiterlimit: 'stroke-miterlimit',\n strokeOpacity: 'stroke-opacity',\n fillOpacity: 'fill-opacity',\n fillRule: 'fill-rule',\n clipPath: 'clip-path',\n clipRule: 'clip-rule',\n colorInterpolation: 'color-interpolation',\n colorInterpolationFilters: 'color-interpolation-filters',\n floodColor: 'flood-color',\n floodOpacity: 'flood-opacity',\n lightingColor: 'lighting-color',\n stopColor: 'stop-color',\n stopOpacity: 'stop-opacity',\n shapeRendering: 'shape-rendering',\n imageRendering: 'image-rendering',\n textRendering: 'text-rendering',\n pointerEvents: 'pointer-events',\n vectorEffect: 'vector-effect',\n paintOrder: 'paint-order',\n\n // SVG text/font attributes\n fontFamily: 'font-family',\n fontSize: 'font-size',\n fontStyle: 'font-style',\n fontVariant: 'font-variant',\n fontWeight: 'font-weight',\n fontStretch: 'font-stretch',\n textAnchor: 'text-anchor',\n textDecoration: 'text-decoration',\n dominantBaseline: 'dominant-baseline',\n alignmentBaseline: 'alignment-baseline',\n baselineShift: 'baseline-shift',\n letterSpacing: 'letter-spacing',\n wordSpacing: 'word-spacing',\n writingMode: 'writing-mode',\n\n // SVG marker attributes\n markerStart: 'marker-start',\n markerMid: 'marker-mid',\n markerEnd: 'marker-end',\n\n // SVG xlink (legacy but still used)\n xlinkHref: 'xlink:href',\n xlinkShow: 'xlink:show',\n xlinkActuate: 'xlink:actuate',\n xlinkType: 'xlink:type',\n xlinkRole: 'xlink:role',\n xlinkTitle: 'xlink:title',\n xlinkArcrole: 'xlink:arcrole',\n xmlBase: 'xml:base',\n xmlLang: 'xml:lang',\n xmlSpace: 'xml:space',\n xmlnsXlink: 'xmlns:xlink',\n};\n\nfunction renderAttr(key: string, value: unknown): string {\n const name = ATTR_ALIASES[key] ?? key;\n if (value == null || value === false) return '';\n if (value === true) return ` ${name}`;\n let strValue: string;\n if (value instanceof SafeHtml) {\n strValue = value.__html;\n } else if (typeof value === 'number') {\n strValue = String(value);\n } else if (typeof value === 'string') {\n strValue = escapeAttr(value);\n } else {\n throw new Error(\n `JSX: unsupported value for attribute \"${key}\" — got ${describeValue(value)}. `\n + 'Attribute values must be string, number, boolean, null, undefined, or SafeHtml. '\n + 'Did you mean to read .value off a Signal, or stringify the object first?',\n );\n }\n return ` ${name}=\"${strValue}\"`;\n}\n\nexport function jsx(tag: string | ((props: Props) => SafeHtml), props: Props): SafeHtml {\n if (typeof tag === 'function') return tag(props);\n\n const { children, ...attrs } = props;\n const attrStr = Object.entries(attrs)\n .map(([k, v]) => renderAttr(k, v))\n .join('');\n\n if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);\n\n const childStr = children != null ? renderChildren(children) : '';\n return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);\n}\n\nexport { jsx as jsxs };\n// vitest's dev-mode JSX transform emits `jsxDEV(tag, props, ...)`; the\n// alias lets tests import this module without the production build pipeline\n// caring.\nexport { jsx as jsxDEV };\n\nexport function Fragment({ children }: { children?: Children }): SafeHtml {\n return new SafeHtml(children != null ? renderChildren(children) : '');\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace JSX {\n export type Element = SafeHtml;\n export interface ElementChildrenAttribute {\n children: unknown;\n }\n export interface IntrinsicElements {\n [elemName: string]: Record<string, unknown>;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"jsx-runtime.js"}
|
package/dist/testing.js
CHANGED
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"testing.js"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kerfjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
"test:watch": "vitest",
|
|
61
61
|
"test:unit": "vitest run tests/unit --coverage",
|
|
62
62
|
"test:integration": "vitest run tests/integration --coverage",
|
|
63
|
+
"test:dist": "npm run build && vitest run --config vitest.config.dist.ts",
|
|
64
|
+
"test:dist:full": "npm run build && vitest run --config vitest.config.dist-full.ts",
|
|
63
65
|
"lint": "eslint src tests",
|
|
64
66
|
"typecheck": "tsc --noEmit",
|
|
65
67
|
"clean": "rm -rf dist coverage node_modules/.cache",
|