@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 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: ~<!--bm:view.vsReact-->2.2<!--/bm-->× faster than React's production `createElement`**, ~<!--bm:view.vsPreact-->2.8<!--/bm-->× 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 **~<!--bm:view.engineCost-->23.2<!--/bm-->× 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.
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.
@@ -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.49.2",
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.49.2"
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
- patchNode(ctx, parent, oldDom[oldEnd], oE, nE, ns);
1108
- tailRef = oldDom[oldEnd];
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)) {
@@ -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' && themes[nameOrOverrides.theme]) {
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
- let out = '<' + tag + serializeProps(props, SKIP_PROPS, policy, onUnsafe);
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);