@jarenjs/view 0.49.2 → 0.66.1
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/README.md +23 -2
- package/docs/VIEW-FORMAT.md +5 -0
- package/package.json +2 -2
- package/src/dom.js +3 -2
- package/src/helpers/theme.js +3 -2
- package/src/html.js +12 -1
package/README.md
CHANGED
|
@@ -168,14 +168,35 @@ The patcher's first check is `oldVnode === newVnode` — a reference-equal subtr
|
|
|
168
168
|
|
|
169
169
|
Measured, not claimed — run `npm run benchmark:view` for your own numbers, and the [benchmarks page](https://jarenjs.github.io/#/benchmarks?suite=view) publishes the latest full run with its date and machine. Two different costs get measured on a 1000-row table (output equality with React and preact asserted before timing), and keeping them apart is what makes the numbers readable:
|
|
170
170
|
|
|
171
|
-
- **The format.** A vnode is a tagged array — an array literal plus a plain object — and the renderer, patcher and SSR accept it from any producer. A **hand-written view building tagged arrays directly is the fastest element builder in the table: ~<!--
|
|
172
|
-
- **The engine.** A JSLT stylesheet is a view as *data* — schema-validated, serializable, storable, and renderable from an untrusted source through the [safe profile](#untrusted-views-the-safe-profile) — and running that document through the generic dispatcher costs **~<!--
|
|
171
|
+
- **The format.** A vnode is a tagged array — an array literal plus a plain object — and the renderer, patcher and SSR accept it from any producer. A **hand-written view building tagged arrays directly is the fastest element builder in the table: ~<!--fact:view.vsReact-->2.2<!--/fact-->× faster than React's production `createElement`**, ~<!--fact:view.vsPreact-->2.8<!--/fact-->× faster than preact's `h()`. Writing views by hand is fully supported; the stylesheet is opt-in per view.
|
|
172
|
+
- **The engine.** A JSLT stylesheet is a view as *data* — schema-validated, serializable, storable, and renderable from an untrusted source through the [safe profile](#untrusted-views-the-safe-profile) — and running that document through the generic dispatcher costs **~<!--fact:view.engineCost-->23.2<!--/fact-->× the hand-written build**. That is the published price of a capability none of the rivals has a mode for: a JSX view is code by construction, so there is no data-driven React number to compare against.
|
|
173
173
|
- **The re-render path.** An **unchanged document re-renders in O(1)** — the memoized transform returns the previous output by reference and the patcher skips it whole — and the vnode-level `memo` marker gives hand-written producers the same subtree skip (~12× over the child scan). For a one-row copy-on-write update the memo cuts the stylesheet frame ~1.5× against its own no-memo path; at this table size the hand-written view plus a full diff is still the fastest frame outright, and that is on the page too. SSR lands within ~1.6–2.8× of `preact-render-to-string` depending on the route, on byte-identical output.
|
|
174
174
|
|
|
175
175
|
When a producer cannot preserve the reference — it rebuilds its tree but knows a region did not change — the **`memo` prop** says so declaratively (VIEW-FORMAT §5.5): two same-node vnodes carrying equal `memo` values skip reconciliation exactly like reference-equal ones. It is a producer-owned assertion, `key`'s sibling: equal markers promise identical subtrees, and a violated promise means stale output. Measured on a reallocated parent over 10 000 shared children, the marker removes the whole per-child scan: about **448 µs** to walk the children looking for `===` skips versus about **41 µs** with the marker — **10.9×**, and the gap widens with the child count (`npm run benchmark:view`). `@jarenjs/charts`' streaming sessions are the reference consumer.
|
|
176
176
|
|
|
177
177
|
Children reconcile with a head/tail sweep plus a key map for the middle: keyed siblings move their real DOM nodes instead of recreating them; unkeyed siblings patch positionally. Event bindings are data stored on the node behind one shared proxy listener per event type — re-rendering rebinds by assignment, never through `addEventListener`.
|
|
178
178
|
|
|
179
|
+
## Exports
|
|
180
|
+
|
|
181
|
+
Every subpath a consumer can import, derived from the manifest by
|
|
182
|
+
`npm run docs:derive` (`npm run docs:check` fails when the two drift):
|
|
183
|
+
|
|
184
|
+
<!--fact:exports.view-->
|
|
185
|
+
| Import | Kind | Declarations |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `@jarenjs/view` | JavaScript | declared |
|
|
188
|
+
| `@jarenjs/view/helpers` | JavaScript | declared |
|
|
189
|
+
| `@jarenjs/view/helpers/index` | JavaScript | declared |
|
|
190
|
+
| `@jarenjs/view/helpers/memo` | JavaScript | declared |
|
|
191
|
+
| `@jarenjs/view/helpers/metrics` | JavaScript | declared |
|
|
192
|
+
| `@jarenjs/view/helpers/svg` | JavaScript | declared |
|
|
193
|
+
| `@jarenjs/view/helpers/theme` | JavaScript | declared |
|
|
194
|
+
| `@jarenjs/view/helpers/url` | JavaScript | declared |
|
|
195
|
+
| `@jarenjs/view/schemas/jaren-vnode-safe.schema.json` | schema | — |
|
|
196
|
+
| `@jarenjs/view/schemas/jaren-vnode.schema.json` | schema | — |
|
|
197
|
+
| `@jarenjs/view/package.json` | metadata | — |
|
|
198
|
+
<!--/fact-->
|
|
199
|
+
|
|
179
200
|
## Development
|
|
180
201
|
|
|
181
202
|
Unit tests live in `test/view/` at the repository root (`npm run test:view`), including the minimal DOM stub they run against. See the repository [README](../../README.md) for the full suite documentation and [ROADMAP](../../docs/ROADMAP.md) for planned work: fragment roots, DOM-adopting hydration, and the memoized rule-output layer that turns JSLT sharing into cross-frame skipping.
|
package/docs/VIEW-FORMAT.md
CHANGED
|
@@ -225,6 +225,11 @@ patching renderer would build: text and attribute values escaped (`&`,
|
|
|
225
225
|
no output, boolean and style props serialized per §3. Serialization is
|
|
226
226
|
pure: no state, no DOM, safe in any runtime.
|
|
227
227
|
|
|
228
|
+
In trusted mode, a `textarea` carrying a `value` prop serializes that
|
|
229
|
+
value as escaped text content, overriding its children; nullish values
|
|
230
|
+
produce empty text. A leading newline is preserved through HTML parsing.
|
|
231
|
+
Safe mode keeps its attribute-only policy (§8).
|
|
232
|
+
|
|
228
233
|
Hydration in 0.1 is a client-side first render into the same container
|
|
229
234
|
(empty and rebuild). Adopting existing server-rendered DOM is a
|
|
230
235
|
roadmap item, not part of this contract.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/view",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.66.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"ssr"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@jarenjs/core": "^0.
|
|
56
|
+
"@jarenjs/core": "^0.66.1"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "npm run build:types",
|
package/src/dom.js
CHANGED
|
@@ -1104,8 +1104,9 @@ function patchChildren(ctx, parent, oldCh, newCh, ns) {
|
|
|
1104
1104
|
oldStart++; newStart++;
|
|
1105
1105
|
}
|
|
1106
1106
|
else if (isSameNode(oE, nE)) {
|
|
1107
|
-
|
|
1108
|
-
|
|
1107
|
+
// A widget may replace its host while keeping its vnode key/tag.
|
|
1108
|
+
// Future insertions must anchor to the live replacement.
|
|
1109
|
+
tailRef = patchNode(ctx, parent, oldDom[oldEnd], oE, nE, ns);
|
|
1109
1110
|
oldEnd--; newEnd--;
|
|
1110
1111
|
}
|
|
1111
1112
|
else if (isSameNode(oS, nE)) {
|
package/src/helpers/theme.js
CHANGED
|
@@ -51,11 +51,12 @@ export function resolveTheme(themes, prefix, nameOrOverrides = 'default', hostVa
|
|
|
51
51
|
let vars = null;
|
|
52
52
|
if (nameOrOverrides === 'host' && hostVars) nameOrOverrides = { vars: hostVars };
|
|
53
53
|
if (typeof nameOrOverrides === 'string') {
|
|
54
|
-
name = themes[nameOrOverrides] ? nameOrOverrides : 'default';
|
|
54
|
+
name = Object.hasOwn(themes, nameOrOverrides) && themes[nameOrOverrides] ? nameOrOverrides : 'default';
|
|
55
55
|
base = themes[name];
|
|
56
56
|
}
|
|
57
57
|
else if (nameOrOverrides && typeof nameOrOverrides === 'object') {
|
|
58
|
-
if (typeof nameOrOverrides.theme === 'string'
|
|
58
|
+
if (typeof nameOrOverrides.theme === 'string'
|
|
59
|
+
&& Object.hasOwn(themes, nameOrOverrides.theme) && themes[nameOrOverrides.theme]) {
|
|
59
60
|
name = nameOrOverrides.theme;
|
|
60
61
|
base = themes[name];
|
|
61
62
|
}
|
package/src/html.js
CHANGED
|
@@ -29,6 +29,9 @@ const VOID_ELEMENTS = new Set([
|
|
|
29
29
|
/** Props that never serialize to markup. */
|
|
30
30
|
const SKIP_PROPS = new Set(['key', 'on', 'memo']);
|
|
31
31
|
|
|
32
|
+
/** A controlled textarea's value is its text content in HTML markup. */
|
|
33
|
+
const TEXTAREA_SKIP_PROPS = new Set([...SKIP_PROPS, 'value']);
|
|
34
|
+
|
|
32
35
|
/** Widget-vnode props that configure the widget, not the host element. */
|
|
33
36
|
const WIDGET_SKIP_PROPS = new Set(['key', 'on', 'memo', 'name', 'props', 'tag']);
|
|
34
37
|
|
|
@@ -184,11 +187,19 @@ function renderNode(vnode, widgets, policy, onUnsafe) {
|
|
|
184
187
|
if (policy !== null && onUnsafe !== null && props.on !== undefined) {
|
|
185
188
|
onUnsafe({ kind: 'event', name: 'on' });
|
|
186
189
|
}
|
|
187
|
-
|
|
190
|
+
const controlledTextarea = policy === null && tag === 'textarea' && 'value' in props;
|
|
191
|
+
let out = '<' + tag + serializeProps(props,
|
|
192
|
+
controlledTextarea ? TEXTAREA_SKIP_PROPS : SKIP_PROPS, policy, onUnsafe);
|
|
188
193
|
if (VOID_ELEMENTS.has(tag)) {
|
|
189
194
|
return out + '>';
|
|
190
195
|
}
|
|
191
196
|
out += '>';
|
|
197
|
+
if (controlledTextarea) {
|
|
198
|
+
const value = props.value == null ? '' : String(props.value);
|
|
199
|
+
// HTML normalizes CR/CRLF to LF and consumes one leading LF in a
|
|
200
|
+
// textarea. Protect an authored newline with an extra LF.
|
|
201
|
+
return out + (/^[\r\n]/.test(value) ? '\n' : '') + escapeText(value) + '</' + tag + '>';
|
|
202
|
+
}
|
|
192
203
|
const children = childrenOf(vnode);
|
|
193
204
|
for (let i = 0; i < children.length; i++) {
|
|
194
205
|
out += renderNode(children[i], widgets, policy, onUnsafe);
|