tutuca 0.9.67 → 0.9.69

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tutuca",
3
- "version": "0.9.67",
3
+ "version": "0.9.69",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency SPA framework with immutable state and virtual DOM",
6
6
  "main": "./dist/tutuca.js",
@@ -7,6 +7,9 @@ orchestration. Read this file when authoring or reviewing
7
7
  `component({...})` definitions, `view: html\`...\`` templates, macros, or
8
8
  the `tutuca` CLI.
9
9
 
10
+ > Orchestration channels — `bubble`, `send`/`receive`, async
11
+ > `request`/`response`, the `$unknown` fallback, and request-handler
12
+ > registration: see [request-response.md](./request-response.md).
10
13
  > Advanced topics (drag & drop, dynamic bindings `*x`, pseudo-`x` for
11
14
  > `<select>`/`<table>`/`<tr>`, custom seq types, Tailwind/MargaUI
12
15
  > compilation): see [advanced.md](./advanced.md). CLI commands, flags,
@@ -173,8 +176,8 @@ to the value the handler should run against. The same `Path` is reused
173
176
  verbatim for `ctx.send`, `ctx.bubble`, and `ctx.request` /
174
177
  response: because it's positional rather than a captured reference, an
175
178
  async response still lands at the right slot even after intervening
176
- transactions have rebuilt the root. See *Bubble Events*, *Send /
177
- Receive*, *Async Requests* for the dispatch APIs.
179
+ transactions have rebuilt the root. See
180
+ [request-response.md](./request-response.md) for the dispatch APIs.
178
181
 
179
182
  **Why `alter` is its own table.** Alter handlers are pure, evaluated
180
183
  on every render, and produce binds (no state change). `input` /
@@ -727,114 +730,53 @@ a same-shape handler block:
727
730
 
728
731
  Every handler is called as `handler(...args, ctx)` and returns a
729
732
  (possibly updated) instance of `this`; the framework swaps the
730
- returned value into the dispatch path. The four sections below cover
731
- each channel in turn.
733
+ returned value into the dispatch path. The three event-driven channels
734
+ beyond `input` `bubble`, `send`/`receive`, async `request`/`response`
735
+ — plus the shared `$unknown` fallback and request-handler registration
736
+ are documented in [request-response.md](./request-response.md); the
737
+ brief anchors below cover the essentials.
732
738
 
733
739
  `alter` is a fifth handler block, but unlike the four above it isn't
734
740
  event-triggered — the renderer invokes alter handlers to produce
735
741
  binds, not to update state. See *Mental model* and *Scope Enrichment*.
736
742
 
737
- ## Bubble Events
738
-
739
- ```js
740
- input: { onClick(ctx) { ctx.bubble("treeItemSelected", [this]); return this; } },
741
- bubble: {
742
- treeItemSelected(selected, ctx) { // ctx.stopPropagation() to halt
743
- return this.insertInLogAt(0, `selected ${selected.label}`);
744
- },
745
- }
746
- ```
747
-
748
- `ctx.bubble("name", args)` emits an event that walks the dispatch path
749
- back toward the root. Each ancestor whose component defines
750
- `bubble.<name>(...args, ctx)` runs it (others are skipped silently);
751
- bubbling stops at the root or when a handler calls
752
- `ctx.stopPropagation()`. Ancestors see the event *after* descendants
753
- have transacted, so bubble handlers are the place for aggregate state
754
- (logs, selections, totals).
755
-
756
- When to bubble: handle the event locally if the current component owns
757
- the state needed to respond. Bubble when the action belongs to an
758
- ancestor (a list item's "remove" must reach the list that owns the
759
- items), or when an ancestor may want to react to or record something
760
- that happened (selection, logging, analytics). Don't bubble events
761
- with no consumer.
762
-
763
- ## Send / Receive
764
-
765
- `ctx.send(name, args)` delivers a message to a specific target
766
- component (addressed by path; on its own `ctx.send` targets `this`).
767
- The target's `receive.<name>(...args, ctx)` handler runs. There is
768
- **no built-in lifecycle** — `receive.init` is just a convention; the
769
- host must dispatch it (typically after `app.start()`) for it to run.
770
-
771
- ```js
772
- receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } }
773
- ```
774
-
775
- Dispatch from anywhere:
776
-
777
- ```js
778
- app.sendAtRoot("init"); // host code, top-level
779
- ctx.at.field("personalSite").send("init"); // child by field name
780
- ctx.at.index("items", 3).send("init"); // list element at index 3
781
- ctx.at.key("byKey", "k1").send("init"); // map entry by key
782
- ctx.at.field("a").field("b").index("xs", 0).send("ping"); // chain freely
783
- ctx.send("name"); // self
784
- ctx.bubble("name", [arg]); // bubble up
785
- ```
786
-
787
- `ctx.at` returns a `PathBuilder` with `.field(name)`, `.index(name, i)`,
788
- and `.key(name, k)`. Each call appends a step to the path before
789
- `.send(...)` / `.bubble(...)` fires; the handler runs inside the child
790
- instance with `this` bound to it. Paths are positional, not references —
791
- see *Mental model* for why this matters across async boundaries.
792
-
793
- When to send: bubble emits an *event* that any ancestor with a
794
- matching handler can observe; send delivers a *message* to one
795
- specific target (or to self). Reach for `ctx.at.…send("name")` when
796
- one component needs to address another by path — e.g. a form telling
797
- its email field to focus after a failed submit
798
- (`ctx.at.field("email").send("focus")`), or a list telling item 3 to
799
- enter edit mode (`ctx.at.index("items", 3).send("startEditing")`).
800
- Reach for `ctx.send("name")` on self to reuse a handler from multiple
801
- call sites without duplicating its body — e.g. a "Reload" button and
802
- `receive.init` both calling `ctx.send("loadData")`. Don't `send` to
803
- self when a direct method call on the same component would do.
804
-
805
- ## Async Requests
806
-
807
- `ctx.request("name", args)` triggers a host-registered async handler
808
- and routes the result back to the issuing component's
809
- `response.<name>(res, err, ctx)`. Use it for fetch / timer / IndexedDB
810
- work that should land back in component state.
811
-
812
- ```js
813
- export function getRequestHandlers() {
814
- return {
815
- async loadData() {
816
- const r = await fetch("https://example.com/data.json");
817
- return await r.json();
818
- },
819
- };
820
- }
821
-
822
- // register at the same scope where you registerComponents
823
- const scope = app.registerComponents([Comp]);
824
- scope.registerRequestHandlers(getRequestHandlers());
825
- ```
826
-
827
- In a component:
828
-
829
- ```js
830
- receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } },
831
- response: { loadData(res, err, ctx) { return this.setIsLoading(false).setItems(res); } },
832
- // override response handler names per-call:
833
- // ctx.request("loadData", [], { onOkName: "loadDataOk", onErrorName: "loadDataErr" });
834
- ```
835
-
836
- The `ctx` arg is the last argument of every `response` / `bubble` /
837
- `receive` handler.
743
+ ## Orchestration channels (bubble / send-receive / request-response)
744
+
745
+ Beyond local `input` handlers, three channels move state between
746
+ components. Full mechanics when-to-use guidance, the `ctx.at`
747
+ `PathBuilder`, error handling, per-call handler-name overrides, the
748
+ `$unknown` fallback, and request-handler registration are in
749
+ [request-response.md](./request-response.md). The essentials:
750
+
751
+ - **`bubble`** — `ctx.bubble("name", args)` walks the dispatch path
752
+ toward the root; each ancestor with `bubble.<name>(...args, ctx)`
753
+ runs (after descendants transact); `ctx.stopPropagation()` halts it.
754
+ Use for aggregate state owned by an ancestor (logs, selections).
755
+
756
+ ```js
757
+ input: { onClick(ctx) { ctx.bubble("itemSelected", [this]); return this; } },
758
+ bubble: { itemSelected(item, ctx) { return this.insertInLogAt(0, item.label); } },
759
+ ```
760
+
761
+ - **`send` / `receive`** — `ctx.send("name", args)` delivers a message
762
+ to one target (self by default, or `ctx.at.field("x").send(...)` /
763
+ `.index(name, i)` / `.key(name, k)` for another); the target's
764
+ `receive.<name>(...args, ctx)` runs. `receive.init` is a convention,
765
+ not a lifecycle hook dispatch it via `app.sendAtRoot("init")`.
766
+
767
+ - **`request` / `response`** — `ctx.request("name", args)` runs a
768
+ host-registered async handler (registered with
769
+ `scope.registerRequestHandlers({...})`) and routes the result to
770
+ `response.<name>(res, err, ctx)` — `res` set on success, `err` on
771
+ failure. Use for fetch / timer / IndexedDB work.
772
+
773
+ ```js
774
+ receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } },
775
+ response: { loadData(res, err, ctx) { return this.setIsLoading(false).setItems(res); } },
776
+ ```
777
+
778
+ `ctx` is always the last argument of every `bubble` / `receive` /
779
+ `response` handler.
838
780
 
839
781
  ## Macros
840
782
 
@@ -53,7 +53,12 @@ template/styling tweaks; `tutuca render <module>` covers those.
53
53
  arguments the handler receives differ:
54
54
  - `receive.<name>(ctx)` — `ctx` carries `send` / `request` / `bubble`.
55
55
  - `bubble.<name>(payload, ctx)` — `payload` is whatever the child sent.
56
- - `response.<name>(res, err, ctx)` — async result + error.
56
+ - `response.<name>(res, err, ctx)` — async result + error. But a
57
+ handler reached via a request's `onOkName` / `onErrorName`
58
+ override takes a **single** payload arg, not `(res, err)`:
59
+ `Comp.response.loadDataOk.call(comp, res)` /
60
+ `Comp.response.loadDataErr.call(comp, err)`. See
61
+ [request-response.md](./request-response.md).
57
62
  - `alter.<name>(...)` — iteration handlers used by `@when`,
58
63
  `@loop-with`, `@enrich-with`. Each kind has its own signature; see
59
64
  *Testing iteration handlers* below.
@@ -266,5 +271,7 @@ export function getTests({ describe, test, expect }) {
266
271
 
267
272
  - [core.md](./core.md) — *Verifying changes*, *Event Handling*,
268
273
  *Component Skeleton*.
274
+ - [request-response.md](./request-response.md) — handler signatures for
275
+ `bubble` / `receive` / `response`, override forms, `$unknown`.
269
276
  - [cli.md](./cli.md) — `test` flags, exit codes, output formats,
270
277
  `--grep` syntax.
@@ -1173,10 +1173,18 @@ class RequestHandler {
1173
1173
 
1174
1174
  // src/vdom.js
1175
1175
  var HTML_NS = "http://www.w3.org/1999/xhtml";
1176
+ var SVG_NS = "http://www.w3.org/2000/svg";
1177
+ var MATH_NS = "http://www.w3.org/1998/Math/MathML";
1176
1178
  var isNamespaced = (node) => {
1177
1179
  const ns = node.namespaceURI;
1178
1180
  return ns !== null && ns !== HTML_NS;
1179
1181
  };
1182
+ var isForeignObject = (tag) => tag.length === 13 && tag.toLowerCase() === "foreignobject";
1183
+ var effectiveNs = (vnode, opts) => vnode.namespace ?? opts.namespace ?? null;
1184
+ function childOpts(vnode, ns, opts) {
1185
+ const target = ns === SVG_NS && isForeignObject(vnode.tag) ? null : ns;
1186
+ return target === (opts.namespace ?? null) ? opts : { ...opts, namespace: target };
1187
+ }
1180
1188
  var NEVER_ASSIGN = new Set([
1181
1189
  "width",
1182
1190
  "height",
@@ -1190,7 +1198,7 @@ var NEVER_ASSIGN = new Set([
1190
1198
  "role",
1191
1199
  "popover"
1192
1200
  ]);
1193
- function applyProperties(node, props, _previous) {
1201
+ function applyProperties(node, props) {
1194
1202
  const namespaced = isNamespaced(node);
1195
1203
  for (const name in props)
1196
1204
  setProp(node, name, props[name], namespaced);
@@ -1199,8 +1207,11 @@ function setProp(node, name, value, namespaced) {
1199
1207
  if (name === "dangerouslySetInnerHTML") {
1200
1208
  if (value === undefined)
1201
1209
  node.replaceChildren();
1202
- else
1203
- node.innerHTML = value.__html ?? "";
1210
+ else {
1211
+ const html = value.__html ?? "";
1212
+ if (html !== node.innerHTML)
1213
+ node.innerHTML = html;
1214
+ }
1204
1215
  return;
1205
1216
  }
1206
1217
  if (typeof value === "function")
@@ -1216,6 +1227,13 @@ function setProp(node, name, value, namespaced) {
1216
1227
  else
1217
1228
  node.setAttribute(name, value);
1218
1229
  }
1230
+ function applyValueLast(node, value) {
1231
+ if (node.tagName === "PROGRESS" && (value == null || value === 0)) {
1232
+ node.removeAttribute("value");
1233
+ } else {
1234
+ setProp(node, "value", value, isNamespaced(node));
1235
+ }
1236
+ }
1219
1237
 
1220
1238
  class VBase {
1221
1239
  }
@@ -1334,15 +1352,23 @@ class VNode extends VBase {
1334
1352
  }
1335
1353
  toDom(opts) {
1336
1354
  const doc = opts.document;
1337
- const node = this.namespace === null ? doc.createElement(this.tag) : doc.createElementNS(this.namespace, this.tag);
1338
- if (this.tag === "SELECT" && "value" in this.attrs) {
1339
- const { value, ...rest } = this.attrs;
1340
- applyProperties(node, rest, {});
1341
- appendChildNodes(node, this.childs, opts);
1342
- applyProperties(node, { value }, {});
1355
+ const ns = effectiveNs(this, opts);
1356
+ const tag = ns !== null && this.tag === this.tag.toUpperCase() ? this.tag.toLowerCase() : this.tag;
1357
+ const attrs = this.attrs;
1358
+ const createOpts = attrs.is != null ? { is: attrs.is } : undefined;
1359
+ const node = ns === null ? doc.createElement(tag, createOpts) : doc.createElementNS(ns, tag, createOpts);
1360
+ const cOpts = childOpts(this, ns, opts);
1361
+ if ("value" in attrs || "checked" in attrs) {
1362
+ const { value, checked, ...rest } = attrs;
1363
+ applyProperties(node, rest);
1364
+ appendChildNodes(node, this.childs, cOpts);
1365
+ if (value !== undefined)
1366
+ applyValueLast(node, value);
1367
+ if (checked !== undefined)
1368
+ setProp(node, "checked", checked, false);
1343
1369
  } else {
1344
- applyProperties(node, this.attrs, {});
1345
- appendChildNodes(node, this.childs, opts);
1370
+ applyProperties(node, attrs);
1371
+ appendChildNodes(node, this.childs, cOpts);
1346
1372
  }
1347
1373
  return node;
1348
1374
  }
@@ -1379,18 +1405,37 @@ function morphNode(domNode, source, target, opts) {
1379
1405
  }
1380
1406
  if (type === 1 && source.isSameKind(target)) {
1381
1407
  const propsDiff = diffProps(source.attrs, target.attrs);
1382
- const isSelect = source.tag === "SELECT";
1408
+ let pendingValue;
1409
+ let pendingChecked;
1410
+ let applyValue = false;
1411
+ let applyChecked = false;
1383
1412
  if (propsDiff) {
1384
- if (isSelect && "value" in propsDiff) {
1385
- const { value: _v, ...rest } = propsDiff;
1386
- applyProperties(domNode, rest, source.attrs);
1413
+ if ("value" in propsDiff || "checked" in propsDiff) {
1414
+ const { value, checked, ...rest } = propsDiff;
1415
+ applyProperties(domNode, rest);
1416
+ if ("value" in propsDiff) {
1417
+ pendingValue = value;
1418
+ applyValue = true;
1419
+ }
1420
+ if ("checked" in propsDiff) {
1421
+ pendingChecked = checked;
1422
+ applyChecked = true;
1423
+ }
1387
1424
  } else
1388
- applyProperties(domNode, propsDiff, source.attrs);
1425
+ applyProperties(domNode, propsDiff);
1426
+ }
1427
+ if (!target.attrs.dangerouslySetInnerHTML) {
1428
+ const ns = effectiveNs(target, opts);
1429
+ morphChildren(domNode, source.childs, target.childs, childOpts(target, ns, opts));
1389
1430
  }
1390
- if (!target.attrs.dangerouslySetInnerHTML)
1391
- morphChildren(domNode, source.childs, target.childs, opts);
1392
- if (isSelect && target.attrs.value !== undefined)
1393
- applyProperties(domNode, { value: target.attrs.value }, source.attrs);
1431
+ if (!applyValue && source.tag === "SELECT" && target.attrs.value !== undefined) {
1432
+ pendingValue = target.attrs.value;
1433
+ applyValue = true;
1434
+ }
1435
+ if (applyValue)
1436
+ applyValueLast(domNode, pendingValue);
1437
+ if (applyChecked)
1438
+ setProp(domNode, "checked", pendingChecked, false);
1394
1439
  return domNode;
1395
1440
  }
1396
1441
  if (type === 11) {
@@ -1494,8 +1539,18 @@ function h(tagName, properties, children, namespace) {
1494
1539
  props[propName] = propVal;
1495
1540
  }
1496
1541
  }
1542
+ if (namespace == null) {
1543
+ const lower = tagName.toLowerCase();
1544
+ if (lower === "svg") {
1545
+ namespace = SVG_NS;
1546
+ tagName = "svg";
1547
+ } else if (lower === "math") {
1548
+ namespace = MATH_NS;
1549
+ tagName = "math";
1550
+ }
1551
+ }
1497
1552
  const c = tagName.charCodeAt(0);
1498
- const tag = namespace == null && c >= 97 && c <= 122 ? tagName.toUpperCase() : tagName;
1553
+ const tag = namespace == null && c >= 97 && c <= 122 && tagName === tagName.toLowerCase() ? tagName.toUpperCase() : tagName;
1499
1554
  const normalizedChildren = [];
1500
1555
  addChild(normalizedChildren, children);
1501
1556
  return new VNode(tag, props, normalizedChildren, key, namespace);
@@ -1747,16 +1802,16 @@ function parseXOpVal(opName, value, px, parserFn) {
1747
1802
  return val;
1748
1803
  }
1749
1804
  function processXExtras(node, attrs, opName, startIdx, px) {
1750
- const consumed = X_OP_CONSUMED[opName];
1751
- const wrappable = X_OP_WRAPPABLE.has(opName);
1805
+ const { consumed, wrappable } = X_OPS[opName];
1752
1806
  const wrappers = [];
1753
1807
  for (let i = startIdx;i < attrs.length; i++) {
1754
1808
  const a = attrs[i];
1755
1809
  const aName = a.name;
1756
1810
  if (consumed.has(aName))
1757
1811
  continue;
1758
- if (wrappable && X_ATTR_WRAPPERS[aName]) {
1759
- wrappers.push([X_ATTR_WRAPPERS[aName], vp.parseBool(a.value, px)]);
1812
+ const wrapper = wrappable ? X_OPS[aName]?.wrapper : null;
1813
+ if (wrapper) {
1814
+ wrappers.push([wrapper, vp.parseBool(a.value, px)]);
1760
1815
  continue;
1761
1816
  }
1762
1817
  const issueInfo = { op: opName, name: aName, value: a.value };
@@ -1848,6 +1903,12 @@ class RenderViewId extends ANode {
1848
1903
  }
1849
1904
  setDataAttr(_key, _val) {}
1850
1905
  }
1906
+ function dynRenderStep(comp, name, key) {
1907
+ const p = resolveDynProducer(comp, name);
1908
+ if (!p)
1909
+ return null;
1910
+ return key === undefined ? new DynStep(p.producerCompId, p.producerSteps) : new DynEachStep(p.producerCompId, p.producerSteps, key);
1911
+ }
1851
1912
 
1852
1913
  class RenderNode extends RenderViewId {
1853
1914
  render(stack, rx) {
@@ -1855,10 +1916,8 @@ class RenderNode extends RenderViewId {
1855
1916
  return rx.renderIt(newStack, this, "", this.viewId);
1856
1917
  }
1857
1918
  toPathStep(ctx) {
1858
- if (this.val instanceof DynVal) {
1859
- const p = resolveDynProducer(ctx.comp, this.val.name);
1860
- return p ? new DynStep(p.producerCompId, p.producerSteps) : null;
1861
- }
1919
+ if (this.val instanceof DynVal)
1920
+ return dynRenderStep(ctx.comp, this.val.name);
1862
1921
  return super.toPathStep(ctx);
1863
1922
  }
1864
1923
  }
@@ -1874,10 +1933,8 @@ class RenderItNode extends RenderViewId {
1874
1933
  return null;
1875
1934
  const nextNode = next.resolveNode();
1876
1935
  if (nextNode instanceof EachNode && next.hasKey) {
1877
- if (nextNode.val instanceof DynVal) {
1878
- const p = resolveDynProducer(ctx.comp, nextNode.val.name);
1879
- return p ? new DynEachStep(p.producerCompId, p.producerSteps, next.key) : null;
1880
- }
1936
+ if (nextNode.val instanceof DynVal)
1937
+ return dynRenderStep(ctx.comp, nextNode.val.name, next.key);
1881
1938
  return new EachRenderItStep(nextNode.val.name, next.key);
1882
1939
  }
1883
1940
  return null;
@@ -1893,12 +1950,8 @@ class RenderEachNode extends RenderViewId {
1893
1950
  return rx.renderEach(stack, this.iterInfo, this, this.viewId);
1894
1951
  }
1895
1952
  toPathStep(ctx) {
1896
- if (this.val instanceof DynVal) {
1897
- if (!ctx.hasKey)
1898
- return null;
1899
- const p = resolveDynProducer(ctx.comp, this.val.name);
1900
- return p ? new DynEachStep(p.producerCompId, p.producerSteps, ctx.key) : null;
1901
- }
1953
+ if (this.val instanceof DynVal)
1954
+ return ctx.hasKey ? dynRenderStep(ctx.comp, this.val.name, ctx.key) : null;
1902
1955
  return super.toPathStep(ctx);
1903
1956
  }
1904
1957
  static parse(px, vp2, s, as, attrs) {
@@ -2028,17 +2081,18 @@ class IterInfo {
2028
2081
  }
2029
2082
  var filterAlwaysTrue = (_v, _k, _seq) => true;
2030
2083
  var nullLoopWith = (seq) => ({ iterData: { seq } });
2031
- var X_OP_CONSUMED = {
2032
- slot: new Set,
2033
- text: new Set,
2034
- render: new Set(["as"]),
2035
- "render-it": new Set(["as"]),
2036
- "render-each": new Set(["as", "when", "loop-with"]),
2037
- show: new Set,
2038
- hide: new Set
2084
+ function xOp(consumed = [], { wrappable = false, wrapper = null } = {}) {
2085
+ return { consumed: new Set(consumed), wrappable, wrapper };
2086
+ }
2087
+ var X_OPS = {
2088
+ slot: xOp(),
2089
+ text: xOp([], { wrappable: true }),
2090
+ render: xOp(["as"], { wrappable: true }),
2091
+ "render-it": xOp(["as"], { wrappable: true }),
2092
+ "render-each": xOp(["as", "when", "loop-with"], { wrappable: true }),
2093
+ show: xOp([], { wrapper: ShowNode }),
2094
+ hide: xOp([], { wrapper: HideNode })
2039
2095
  };
2040
- var X_OP_WRAPPABLE = new Set(["text", "render", "render-it", "render-each"]);
2041
- var X_ATTR_WRAPPERS = { show: ShowNode, hide: HideNode };
2042
2096
  var WRAPPER_NODES = {
2043
2097
  slot: SlotNode,
2044
2098
  show: ShowNode,
@@ -2126,29 +2180,30 @@ var isBlockDomNode = (n) => {
2126
2180
  const node = n instanceof FragmentNode ? n.childs[0] : n;
2127
2181
  return node instanceof DomNode && HTML_BLOCK_TAGS.has(node.tagName);
2128
2182
  };
2183
+ var isEmptyText = (c) => c instanceof TextNode && c.val === "";
2184
+ function trimEdgeWhite(node) {
2185
+ if (!node.isWhiteSpace?.())
2186
+ return false;
2187
+ node.condenseWhiteSpace();
2188
+ return true;
2189
+ }
2129
2190
  function condenseChildsWhites(childs) {
2130
2191
  if (childs.length === 0)
2131
2192
  return childs;
2132
- let changed = false;
2133
- if (childs[0].isWhiteSpace?.()) {
2134
- childs[0].condenseWhiteSpace();
2135
- changed = true;
2136
- }
2137
2193
  const last = childs.length - 1;
2138
- if (last > 0 && childs[last].isWhiteSpace?.()) {
2139
- childs[last].condenseWhiteSpace();
2140
- changed = true;
2141
- }
2194
+ let emptied = trimEdgeWhite(childs[0]);
2195
+ if (last > 0 && trimEdgeWhite(childs[last]))
2196
+ emptied = true;
2142
2197
  for (let i = 1;i < last; i++) {
2143
2198
  const cur = childs[i];
2144
- if (cur.isWhiteSpace?.() && cur.hasNewLine()) {
2145
- const bothBlock = isBlockDomNode(childs[i - 1]) && isBlockDomNode(childs[i + 1]);
2146
- cur.condenseWhiteSpace(bothBlock ? "" : " ");
2147
- if (bothBlock)
2148
- changed = true;
2149
- }
2199
+ if (!(cur.isWhiteSpace?.() && cur.hasNewLine()))
2200
+ continue;
2201
+ const bothBlock = isBlockDomNode(childs[i - 1]) && isBlockDomNode(childs[i + 1]);
2202
+ cur.condenseWhiteSpace(bothBlock ? "" : " ");
2203
+ if (bothBlock)
2204
+ emptied = true;
2150
2205
  }
2151
- return changed ? childs.filter((c) => !(c instanceof TextNode && c.val === "")) : childs;
2206
+ return emptied ? childs.filter((c) => !isEmptyText(c)) : childs;
2152
2207
  }
2153
2208
 
2154
2209
  class View {