tutuca 0.9.27 → 0.9.29

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
@@ -7,7 +7,7 @@ Zero-dependency batteries included SPA framework.
7
7
  - **Fits in your head** (and the context window)
8
8
  - **View source friendly** — step through the whole stack
9
9
  - **As much HTML as possible, as little JS as needed**
10
- - ~107KB minified, ~29KB brotli compressed
10
+ - ~169KB minified, ~37KB brotli compressed
11
11
 
12
12
  ## Quick Start
13
13
 
@@ -64,17 +64,17 @@ Zero-dependency batteries included SPA framework.
64
64
  Tutuca ships a single-file CLI (`dist/tutuca-cli.js`) for inspecting, linting,
65
65
  documenting, and rendering components defined in an ES module. The module just
66
66
  needs to export `getComponents()` and, for render-time commands, `getExamples()`
67
- in the storybook shape `{ title, description?, groups?, items: [{ title, description?, value, view? }] }`.
67
+ in the storybook shape `{ title, description?, items: [{ title, description?, value, view? }] }` (a single section, or an array of sections).
68
68
 
69
69
  ### Setup
70
70
 
71
71
  ```sh
72
- npm install --save-dev tutuca jsdom
72
+ npm install --save-dev tutuca
73
73
  # prettier is optional, only needed for --pretty
74
74
  npm install --save-dev prettier
75
75
  ```
76
76
 
77
- The package exposes `tutuca` via `bin`, so `npx tutuca` (or a global `npm i -g tutuca jsdom`) just works. `jsdom` is a peer dep because it's only needed for `render`, `lint`, and `doctor`.
77
+ The package exposes `tutuca` via `bin`, so `npx tutuca` (or a global `npm i -g tutuca`) just works. `jsdom` ships as a regular dependency (it's needed by `render`, `lint`, and `doctor`) and is installed automatically.
78
78
 
79
79
  ### Commands
80
80
 
@@ -221,15 +221,11 @@ function normalizeModule(mod, { path = null } = {}) {
221
221
  "getMacros",
222
222
  "getRequestHandlers",
223
223
  "getExamples",
224
- "getStoryBookSection",
225
224
  "getRoot"
226
225
  ]) {
227
226
  if (typeof mod[key] === "function")
228
227
  present.add(key);
229
228
  }
230
- if (present.has("getStoryBookSection") && !present.has("getExamples")) {
231
- throw shapeError("module exports getStoryBookSection; rename it to getExamples.", "module");
232
- }
233
229
  const components = present.has("getComponents") ? mod.getComponents() : [];
234
230
  const macros = present.has("getMacros") ? mod.getMacros() : null;
235
231
  const requestHandlers = present.has("getRequestHandlers") ? mod.getRequestHandlers() : null;
@@ -1376,13 +1372,13 @@ class IterInfo {
1376
1372
  }
1377
1373
 
1378
1374
  class ParseContext {
1379
- constructor(DOMParser, Text, Comment, nodes, events, macroNodes, frame, parent) {
1375
+ constructor(document2, Text, Comment, nodes, events, macroNodes, frame, parent) {
1380
1376
  this.nodes = nodes ?? [];
1381
1377
  this.events = events ?? [];
1382
1378
  this.macroNodes = macroNodes ?? [];
1383
1379
  this.parent = parent ?? null;
1384
1380
  this.frame = frame ?? {};
1385
- this.DOMParser = DOMParser ?? globalThis.DOMParser;
1381
+ this.document = document2 ?? globalThis.document;
1386
1382
  this.Text = Text ?? globalThis.Text;
1387
1383
  this.Comment = Comment ?? globalThis.Comment;
1388
1384
  this.cacheConstNodes = true;
@@ -1391,12 +1387,14 @@ class ParseContext {
1391
1387
  return this.frame.macroName === name || this.parent?.isInsideMacro(name);
1392
1388
  }
1393
1389
  enterMacro(macroName, macroVars, macroSlots) {
1394
- const { DOMParser: DP, Text, Comment, nodes, events, macroNodes } = this;
1390
+ const { document: document2, Text, Comment, nodes, events, macroNodes } = this;
1395
1391
  const frame = { macroName, macroVars, macroSlots };
1396
- return new ParseContext(DP, Text, Comment, nodes, events, macroNodes, frame, this);
1392
+ return new ParseContext(document2, Text, Comment, nodes, events, macroNodes, frame, this);
1397
1393
  }
1398
- newDOMParser() {
1399
- return new this.DOMParser;
1394
+ parseHTML(html) {
1395
+ const t = this.document.createElement("template");
1396
+ t.innerHTML = html;
1397
+ return Array.from(t.content.childNodes);
1400
1398
  }
1401
1399
  addNodeIf(Class, val, extra) {
1402
1400
  if (val !== null) {
@@ -1442,26 +1440,28 @@ class ParseContext {
1442
1440
  onAttributes(_attrs, _wrapperAttrs, _textChild, _isMacroCall) {}
1443
1441
  }
1444
1442
  function condenseChildsWhites(childs) {
1445
- let end = childs.length;
1446
- if (end === 0)
1443
+ if (childs.length === 0)
1447
1444
  return childs;
1448
- let start = 0;
1449
1445
  let changed = false;
1450
- if (isTextNodeAllBlanks(childs[0])) {
1451
- start = 1;
1446
+ if (childs[0].isWhiteSpace?.()) {
1447
+ childs[0].condenseWhiteSpace();
1452
1448
  changed = true;
1453
1449
  }
1454
- if (end > 1 && isTextNodeAllBlanks(childs[end - 1])) {
1455
- end -= 1;
1450
+ const last = childs.length - 1;
1451
+ if (last > 0 && childs[last].isWhiteSpace?.()) {
1452
+ childs[last].condenseWhiteSpace();
1456
1453
  changed = true;
1457
1454
  }
1458
- for (let i = 1;i < end - 1; i++) {
1455
+ for (let i = 1;i < last; i++) {
1459
1456
  const cur = childs[i];
1460
- if (isTextNodeAllBlanks(cur) && isFirstDomNode(childs[i - 1]) && isFirstDomNode(childs[i + 1]) && cur.hasNewLine()) {
1461
- cur.condenseWhiteSpace();
1457
+ if (cur.isWhiteSpace?.() && cur.hasNewLine()) {
1458
+ const bothBlock = isBlockDomNode(childs[i - 1]) && isBlockDomNode(childs[i + 1]);
1459
+ cur.condenseWhiteSpace(bothBlock ? "" : " ");
1460
+ if (bothBlock)
1461
+ changed = true;
1462
1462
  }
1463
1463
  }
1464
- return changed ? childs.slice(start, end) : childs;
1464
+ return changed ? childs.filter((c) => !(c instanceof TextNode && c.val === "")) : childs;
1465
1465
  }
1466
1466
 
1467
1467
  class NodeEvents {
@@ -1517,7 +1517,10 @@ function compileModifiers(eventName, names) {
1517
1517
  return w(this, f, args, ctx);
1518
1518
  };
1519
1519
  }
1520
- var TextNode, CommentNode, ChildsNode, DomNode, FragmentNode, maybeFragment = (xs) => xs.length === 1 ? xs[0] : new FragmentNode(xs), VALID_NODE_RE, _parser = null, ANode, MacroNode, RenderViewId, RenderNode, RenderItNode, RenderEachNode, RenderTextNode, RenderOnceNode, WrapperNode, ShowNode, HideNode, PushViewNameNode, SlotNode, ScopeNode, EachNode, filterAlwaysTrue = (_v, _k, _seq) => true, nullLoopWith = (seq) => ({ seq }), WRAPPER_NODES, isTextNodeAllBlanks = (n) => n instanceof TextNode && n.isWhiteSpace(), isFirstDomNode = (n) => n instanceof DomNode || n instanceof FragmentNode && n.childs[0] instanceof DomNode, isMac, fwdIfCtxPred = (pred) => (w) => (that, f, args, ctx) => pred(ctx) ? w(that, f, args, ctx) : that, fwdIfKey = (keyName) => fwdIfCtxPred((ctx) => ctx.e.key === keyName), fwdCtrl, fwdMeta, fwdAlt, metaWraps, MOD_WRAPPERS_BY_EVENT, identityModifierWrapper = (f, _ctx) => f;
1520
+ var TextNode, CommentNode, ChildsNode, DomNode, FragmentNode, maybeFragment = (xs) => xs.length === 1 ? xs[0] : new FragmentNode(xs), VALID_NODE_RE, ANode, MacroNode, RenderViewId, RenderNode, RenderItNode, RenderEachNode, RenderTextNode, RenderOnceNode, WrapperNode, ShowNode, HideNode, PushViewNameNode, SlotNode, ScopeNode, EachNode, filterAlwaysTrue = (_v, _k, _seq) => true, nullLoopWith = (seq) => ({ seq }), WRAPPER_NODES, _htmlBlockTags = "ADDRESS,ARTICLE,ASIDE,BLOCKQUOTE,CAPTION,COL,COLGROUP,DETAILS,DIALOG,DIV,DD,DL,DT,FIELDSET,FIGCAPTION,FIGURE,FOOTER,FORM,H1,H2,H3,H4,H5,H6,HEADER,HGROUP,HR,LEGEND,LI,MAIN,MENU,NAV,OL,P,PRE,SECTION,SUMMARY,TABLE,TBODY,TD,TFOOT,TH,THEAD,TR,UL", HTML_BLOCK_TAGS, isBlockDomNode = (n) => {
1521
+ const node = n instanceof FragmentNode ? n.childs[0] : n;
1522
+ return node instanceof DomNode && HTML_BLOCK_TAGS.has(node.tagName);
1523
+ }, isMac, fwdIfCtxPred = (pred) => (w) => (that, f, args, ctx) => pred(ctx) ? w(that, f, args, ctx) : that, fwdIfKey = (keyName) => fwdIfCtxPred((ctx) => ctx.e.key === keyName), fwdCtrl, fwdMeta, fwdAlt, metaWraps, MOD_WRAPPERS_BY_EVENT, identityModifierWrapper = (f, _ctx) => f;
1521
1524
  var init_anode = __esm(() => {
1522
1525
  init_attribute();
1523
1526
  init_path();
@@ -1546,8 +1549,8 @@ var init_anode = __esm(() => {
1546
1549
  }
1547
1550
  return false;
1548
1551
  }
1549
- condenseWhiteSpace() {
1550
- this.val = "";
1552
+ condenseWhiteSpace(replacement = "") {
1553
+ this.val = replacement;
1551
1554
  }
1552
1555
  isConstant() {
1553
1556
  return true;
@@ -1611,9 +1614,7 @@ var init_anode = __esm(() => {
1611
1614
  return this.val.toPathItem();
1612
1615
  }
1613
1616
  static parse(html, px) {
1614
- _parser ??= px.newDOMParser();
1615
- const nodes = _parser.parseFromString(html, "text/html").body.childNodes;
1616
- return ANode.fromDOM(nodes[0] ?? new px.Text(""), px);
1617
+ return ANode.fromDOM(px.parseHTML(html)[0] ?? new px.Text(""), px);
1617
1618
  }
1618
1619
  static fromDOM(node, px) {
1619
1620
  if (node instanceof px.Text)
@@ -1664,7 +1665,7 @@ var init_anode = __esm(() => {
1664
1665
  if (textChild)
1665
1666
  childs.unshift(new RenderTextNode(null, textChild));
1666
1667
  const domChilds = tag !== "PRE" ? condenseChildsWhites(childs) : childs;
1667
- return wrap(new DomNode(tag.toLowerCase(), nAttrs, domChilds), px, wrappers);
1668
+ return wrap(new DomNode(tag, nAttrs, domChilds), px, wrappers);
1668
1669
  }
1669
1670
  return new CommentNode(`Error: InvalidTagName ${tag}`);
1670
1671
  }
@@ -1840,6 +1841,7 @@ var init_anode = __esm(() => {
1840
1841
  scope: ScopeNode,
1841
1842
  "push-view": PushViewNameNode
1842
1843
  };
1844
+ HTML_BLOCK_TAGS = new Set(_htmlBlockTags.split(","));
1843
1845
  isMac = (globalThis.navigator?.userAgent ?? "").toLowerCase().includes("mac");
1844
1846
  fwdCtrl = fwdIfCtxPred(({ e }) => isMac && e.metaKey || e.ctrlKey);
1845
1847
  fwdMeta = fwdIfCtxPred(({ e }) => e.metaKey);
@@ -2369,8 +2371,8 @@ var init_lint_check = __esm(() => {
2369
2371
  "dragInfo"
2370
2372
  ]);
2371
2373
  LintParseContext = class LintParseContext extends ParseContext {
2372
- constructor(DOMParser, Text, Comment) {
2373
- super(DOMParser, Text, Comment);
2374
+ constructor(document2, Text, Comment) {
2375
+ super(document2, Text, Comment);
2374
2376
  this.attrs = [];
2375
2377
  }
2376
2378
  onAttributes(attrs, wrapperAttrs, textChild, isMacroCall = false) {
@@ -2987,23 +2989,20 @@ function morphNode(domNode, source, target, opts) {
2987
2989
  domNode.data = target.text;
2988
2990
  return domNode;
2989
2991
  }
2990
- if (type === 1 && source.tag === target.tag && source.namespace === target.namespace && source.key === target.key) {
2992
+ if (type === 1 && source.isSameKind(target)) {
2991
2993
  const propsDiff = diffProps(source.attrs, target.attrs);
2992
2994
  const isSelect = source.tag === "SELECT";
2993
2995
  if (propsDiff) {
2994
2996
  if (isSelect && "value" in propsDiff) {
2995
2997
  const { value: _v, ...rest } = propsDiff;
2996
2998
  applyProperties(domNode, rest, source.attrs);
2997
- } else {
2999
+ } else
2998
3000
  applyProperties(domNode, propsDiff, source.attrs);
2999
- }
3000
3001
  }
3001
- if (!target.attrs.dangerouslySetInnerHTML) {
3002
+ if (!target.attrs.dangerouslySetInnerHTML)
3002
3003
  morphChildren(domNode, source.childs, target.childs, opts);
3003
- }
3004
- if (isSelect && target.attrs.value !== undefined) {
3004
+ if (isSelect && target.attrs.value !== undefined)
3005
3005
  applyProperties(domNode, { value: target.attrs.value }, source.attrs);
3006
- }
3007
3006
  return domNode;
3008
3007
  }
3009
3008
  if (type === 11) {
@@ -3082,23 +3081,20 @@ function morphChildren(parentDom, oldChilds, newChilds, opts) {
3082
3081
  if (!used[i] && domNodes[i].parentNode === parentDom)
3083
3082
  parentDom.removeChild(domNodes[i]);
3084
3083
  }
3085
- function render(vnode, container, options) {
3086
- const cached = renderCache.get(container);
3084
+ function render(vnode, container, options, prev) {
3087
3085
  const isFragment = vnode instanceof VFragment;
3088
- if (cached && cached.vnode instanceof VFragment === isFragment) {
3089
- const oldDom = isFragment ? container : cached.dom;
3090
- const newDom = morphNode(oldDom, cached.vnode, vnode, options);
3091
- renderCache.set(container, { vnode, dom: isFragment ? container : newDom });
3092
- return newDom;
3086
+ if (prev && prev.vnode instanceof VFragment === isFragment) {
3087
+ const oldDom = isFragment ? container : prev.dom;
3088
+ const newDom = morphNode(oldDom, prev.vnode, vnode, options);
3089
+ return { vnode, dom: isFragment ? container : newDom };
3093
3090
  }
3094
- renderCache.delete(container);
3095
3091
  const domNode = vnode.toDom(options);
3096
3092
  container.replaceChildren(domNode);
3097
- renderCache.set(container, { vnode, dom: isFragment ? container : domNode });
3098
- return domNode;
3093
+ return { vnode, dom: isFragment ? container : domNode };
3099
3094
  }
3100
3095
  function h(tagName, properties, children) {
3101
- const tag = tagName.toUpperCase();
3096
+ const c = tagName.charCodeAt(0);
3097
+ const tag = c >= 97 && c <= 122 ? tagName.toUpperCase() : tagName;
3102
3098
  const props = {};
3103
3099
  let key, namespace;
3104
3100
  if (properties) {
@@ -3126,7 +3122,7 @@ function h(tagName, properties, children) {
3126
3122
  addChild(normalizedChildren, children);
3127
3123
  return new VNode(tag, props, normalizedChildren, key, namespace);
3128
3124
  }
3129
- var isHtmlAttribute = (propName) => propName[4] === "-" && (propName[0] === "d" || propName[0] === "a"), isObject = (v) => v !== null && typeof v === "object", prototypesDiffer = (a, b) => Object.getPrototypeOf(a) !== Object.getPrototypeOf(b), getKey = (child) => child instanceof VNode ? child.key : undefined, isIterable = (obj) => obj != null && typeof obj !== "string" && typeof obj[Symbol.iterator] === "function", VText, VComment, VFragment, VNode, renderCache;
3125
+ var isHtmlAttribute = (propName) => propName[4] === "-" && (propName[0] === "d" || propName[0] === "a"), isObject = (v) => v !== null && typeof v === "object", prototypesDiffer = (a, b) => Object.getPrototypeOf(a) !== Object.getPrototypeOf(b), getKey = (child) => child instanceof VNode ? child.key : undefined, isIterable = (obj) => obj != null && typeof obj !== "string" && typeof obj[Symbol.iterator] === "function", VText, VComment, VFragment, VNode;
3130
3126
  var init_vdom = __esm(() => {
3131
3127
  VText = class VText extends VBase {
3132
3128
  constructor(text) {
@@ -3190,10 +3186,13 @@ var init_vdom = __esm(() => {
3190
3186
  get nodeType() {
3191
3187
  return 1;
3192
3188
  }
3189
+ isSameKind(other) {
3190
+ return this.tag === other.tag && this.namespace === other.namespace && this.key === other.key;
3191
+ }
3193
3192
  isEqualTo(other) {
3194
3193
  if (this === other)
3195
3194
  return true;
3196
- if (!(other instanceof VNode) || this.tag !== other.tag || this.key !== other.key || this.namespace !== other.namespace || this.childs.length !== other.childs.length) {
3195
+ if (!(other instanceof VNode) || !this.isSameKind(other) || this.childs.length !== other.childs.length) {
3197
3196
  return false;
3198
3197
  }
3199
3198
  if (this.attrs !== other.attrs) {
@@ -3221,7 +3220,6 @@ var init_vdom = __esm(() => {
3221
3220
  return node;
3222
3221
  }
3223
3222
  };
3224
- renderCache = new WeakMap;
3225
3223
  });
3226
3224
 
3227
3225
  // src/app.js
@@ -3244,6 +3242,7 @@ class App {
3244
3242
  };
3245
3243
  this._compiled = false;
3246
3244
  this._renderOpts = { document: rootNode.ownerDocument };
3245
+ this._renderState = null;
3247
3246
  }
3248
3247
  get state() {
3249
3248
  return this.transactor.state;
@@ -3350,7 +3349,10 @@ class App {
3350
3349
  render() {
3351
3350
  const root = this.state.val;
3352
3351
  const stack = this.makeStack(root);
3353
- return render(this.renderer.renderRoot(stack, root), this.rootNode, this._renderOpts);
3352
+ const { renderer, rootNode, _renderOpts, _renderState } = this;
3353
+ const newState = render(renderer.renderRoot(stack, root), rootNode, _renderOpts, _renderState);
3354
+ this._renderState = newState;
3355
+ return newState.dom;
3354
3356
  }
3355
3357
  onChange(callback) {
3356
3358
  this.transactor.state.onChange(callback);
@@ -7721,7 +7723,7 @@ class Renderer {
7721
7723
  renderToDOM(stack, val) {
7722
7724
  const rootNode = document.createElement("div");
7723
7725
  const rOpts = { document };
7724
- render(h("div", null, [this.renderRoot(stack, val)]), rootNode, rOpts);
7726
+ render(h("DIV", null, [this.renderRoot(stack, val)]), rootNode, rOpts);
7725
7727
  return rootNode.childNodes[0];
7726
7728
  }
7727
7729
  renderToString(stack, val, cleanAttrs = true) {
@@ -8069,9 +8071,6 @@ MODULE CONVENTION
8069
8071
 
8070
8072
  export function getRoot() // optional; returned by info
8071
8073
 
8072
- The legacy \`getStoryBookSection()\` name fails fast with
8073
- EXAMPLES_SHAPE_MISMATCH — rename it to \`getExamples\`.
8074
-
8075
8074
  COMMANDS (require <module-path>)
8076
8075
  info
8077
8076
  Summarize which getX() exports are present and count components,
@@ -8184,18 +8183,18 @@ init_lint_check();
8184
8183
  import { JSDOM } from "jsdom";
8185
8184
  async function createNodeEnv() {
8186
8185
  const dom = new JSDOM("<!DOCTYPE html><html><head></head><body></body></html>");
8187
- const { DOMParser, Text, Comment } = dom.window;
8188
- globalThis.document = dom.window.document;
8186
+ const { document: document2, Text, Comment } = dom.window;
8187
+ globalThis.document = document2;
8189
8188
 
8190
8189
  class HeadlessParseContext extends ParseContext {
8191
8190
  constructor() {
8192
- super(DOMParser, Text, Comment);
8191
+ super(document2, Text, Comment);
8193
8192
  }
8194
8193
  }
8195
8194
 
8196
8195
  class HeadlessLintParseContext extends LintParseContext {
8197
8196
  constructor() {
8198
- super(DOMParser, Text, Comment);
8197
+ super(document2, Text, Comment);
8199
8198
  }
8200
8199
  }
8201
8200
  return {
@@ -918,8 +918,8 @@ class TextNode extends BaseNode {
918
918
  }
919
919
  return false;
920
920
  }
921
- condenseWhiteSpace() {
922
- this.val = "";
921
+ condenseWhiteSpace(replacement = "") {
922
+ this.val = replacement;
923
923
  }
924
924
  isConstant() {
925
925
  return true;
@@ -993,7 +993,6 @@ class FragmentNode extends ChildsNode {
993
993
  }
994
994
  var maybeFragment = (xs) => xs.length === 1 ? xs[0] : new FragmentNode(xs);
995
995
  var VALID_NODE_RE = /^[a-zA-Z][a-zA-Z0-9-]*$/;
996
- var _parser = null;
997
996
 
998
997
  class ANode extends BaseNode {
999
998
  constructor(nodeId, val) {
@@ -1005,9 +1004,7 @@ class ANode extends BaseNode {
1005
1004
  return this.val.toPathItem();
1006
1005
  }
1007
1006
  static parse(html, px) {
1008
- _parser ??= px.newDOMParser();
1009
- const nodes = _parser.parseFromString(html, "text/html").body.childNodes;
1010
- return ANode.fromDOM(nodes[0] ?? new px.Text(""), px);
1007
+ return ANode.fromDOM(px.parseHTML(html)[0] ?? new px.Text(""), px);
1011
1008
  }
1012
1009
  static fromDOM(node, px) {
1013
1010
  if (node instanceof px.Text)
@@ -1058,7 +1055,7 @@ class ANode extends BaseNode {
1058
1055
  if (textChild)
1059
1056
  childs.unshift(new RenderTextNode(null, textChild));
1060
1057
  const domChilds = tag !== "PRE" ? condenseChildsWhites(childs) : childs;
1061
- return wrap(new DomNode(tag.toLowerCase(), nAttrs, domChilds), px, wrappers);
1058
+ return wrap(new DomNode(tag, nAttrs, domChilds), px, wrappers);
1062
1059
  }
1063
1060
  return new CommentNode(`Error: InvalidTagName ${tag}`);
1064
1061
  }
@@ -1300,13 +1297,13 @@ var WRAPPER_NODES = {
1300
1297
  };
1301
1298
 
1302
1299
  class ParseContext {
1303
- constructor(DOMParser, Text, Comment, nodes, events, macroNodes, frame, parent) {
1300
+ constructor(document2, Text, Comment, nodes, events, macroNodes, frame, parent) {
1304
1301
  this.nodes = nodes ?? [];
1305
1302
  this.events = events ?? [];
1306
1303
  this.macroNodes = macroNodes ?? [];
1307
1304
  this.parent = parent ?? null;
1308
1305
  this.frame = frame ?? {};
1309
- this.DOMParser = DOMParser ?? globalThis.DOMParser;
1306
+ this.document = document2 ?? globalThis.document;
1310
1307
  this.Text = Text ?? globalThis.Text;
1311
1308
  this.Comment = Comment ?? globalThis.Comment;
1312
1309
  this.cacheConstNodes = true;
@@ -1315,12 +1312,14 @@ class ParseContext {
1315
1312
  return this.frame.macroName === name || this.parent?.isInsideMacro(name);
1316
1313
  }
1317
1314
  enterMacro(macroName, macroVars, macroSlots) {
1318
- const { DOMParser: DP, Text, Comment, nodes, events, macroNodes } = this;
1315
+ const { document: document2, Text, Comment, nodes, events, macroNodes } = this;
1319
1316
  const frame = { macroName, macroVars, macroSlots };
1320
- return new ParseContext(DP, Text, Comment, nodes, events, macroNodes, frame, this);
1317
+ return new ParseContext(document2, Text, Comment, nodes, events, macroNodes, frame, this);
1321
1318
  }
1322
- newDOMParser() {
1323
- return new this.DOMParser;
1319
+ parseHTML(html) {
1320
+ const t = this.document.createElement("template");
1321
+ t.innerHTML = html;
1322
+ return Array.from(t.content.childNodes);
1324
1323
  }
1325
1324
  addNodeIf(Class, val, extra) {
1326
1325
  if (val !== null) {
@@ -1365,29 +1364,35 @@ class ParseContext {
1365
1364
  }
1366
1365
  onAttributes(_attrs, _wrapperAttrs, _textChild, _isMacroCall) {}
1367
1366
  }
1368
- var isTextNodeAllBlanks = (n) => n instanceof TextNode && n.isWhiteSpace();
1369
- var isFirstDomNode = (n) => n instanceof DomNode || n instanceof FragmentNode && n.childs[0] instanceof DomNode;
1367
+ var _htmlBlockTags = "ADDRESS,ARTICLE,ASIDE,BLOCKQUOTE,CAPTION,COL,COLGROUP,DETAILS,DIALOG,DIV,DD,DL,DT,FIELDSET,FIGCAPTION,FIGURE,FOOTER,FORM,H1,H2,H3,H4,H5,H6,HEADER,HGROUP,HR,LEGEND,LI,MAIN,MENU,NAV,OL,P,PRE,SECTION,SUMMARY,TABLE,TBODY,TD,TFOOT,TH,THEAD,TR,UL";
1368
+ var HTML_BLOCK_TAGS = new Set(_htmlBlockTags.split(","));
1369
+ var isBlockDomNode = (n) => {
1370
+ const node = n instanceof FragmentNode ? n.childs[0] : n;
1371
+ return node instanceof DomNode && HTML_BLOCK_TAGS.has(node.tagName);
1372
+ };
1370
1373
  function condenseChildsWhites(childs) {
1371
- let end = childs.length;
1372
- if (end === 0)
1374
+ if (childs.length === 0)
1373
1375
  return childs;
1374
- let start = 0;
1375
1376
  let changed = false;
1376
- if (isTextNodeAllBlanks(childs[0])) {
1377
- start = 1;
1377
+ if (childs[0].isWhiteSpace?.()) {
1378
+ childs[0].condenseWhiteSpace();
1378
1379
  changed = true;
1379
1380
  }
1380
- if (end > 1 && isTextNodeAllBlanks(childs[end - 1])) {
1381
- end -= 1;
1381
+ const last = childs.length - 1;
1382
+ if (last > 0 && childs[last].isWhiteSpace?.()) {
1383
+ childs[last].condenseWhiteSpace();
1382
1384
  changed = true;
1383
1385
  }
1384
- for (let i = 1;i < end - 1; i++) {
1386
+ for (let i = 1;i < last; i++) {
1385
1387
  const cur = childs[i];
1386
- if (isTextNodeAllBlanks(cur) && isFirstDomNode(childs[i - 1]) && isFirstDomNode(childs[i + 1]) && cur.hasNewLine()) {
1387
- cur.condenseWhiteSpace();
1388
+ if (cur.isWhiteSpace?.() && cur.hasNewLine()) {
1389
+ const bothBlock = isBlockDomNode(childs[i - 1]) && isBlockDomNode(childs[i + 1]);
1390
+ cur.condenseWhiteSpace(bothBlock ? "" : " ");
1391
+ if (bothBlock)
1392
+ changed = true;
1388
1393
  }
1389
1394
  }
1390
- return changed ? childs.slice(start, end) : childs;
1395
+ return changed ? childs.filter((c) => !(c instanceof TextNode && c.val === "")) : childs;
1391
1396
  }
1392
1397
 
1393
1398
  class View {
@@ -1494,9 +1499,9 @@ class ParseCtxClassSetCollector extends ParseContext {
1494
1499
  }
1495
1500
  }
1496
1501
  enterMacro(macroName, macroVars, macroSlots) {
1497
- const { DOMParser: DP, Text, Comment, nodes, events, macroNodes } = this;
1502
+ const { document: document2, Text, Comment, nodes, events, macroNodes } = this;
1498
1503
  const frame = { macroName, macroVars, macroSlots };
1499
- const v = new ParseCtxClassSetCollector(DP, Text, Comment, nodes, events, macroNodes, frame, this);
1504
+ const v = new ParseCtxClassSetCollector(document2, Text, Comment, nodes, events, macroNodes, frame, this);
1500
1505
  v.classes = this.classes;
1501
1506
  return v;
1502
1507
  }
@@ -1876,8 +1881,8 @@ class LintContext {
1876
1881
  }
1877
1882
 
1878
1883
  class LintParseContext extends ParseContext {
1879
- constructor(DOMParser, Text, Comment) {
1880
- super(DOMParser, Text, Comment);
1884
+ constructor(document2, Text, Comment) {
1885
+ super(document2, Text, Comment);
1881
1886
  this.attrs = [];
1882
1887
  }
1883
1888
  onAttributes(attrs, wrapperAttrs, textChild, isMacroCall = false) {
@@ -2824,10 +2829,13 @@ class VNode extends VBase {
2824
2829
  get nodeType() {
2825
2830
  return 1;
2826
2831
  }
2832
+ isSameKind(other) {
2833
+ return this.tag === other.tag && this.namespace === other.namespace && this.key === other.key;
2834
+ }
2827
2835
  isEqualTo(other) {
2828
2836
  if (this === other)
2829
2837
  return true;
2830
- if (!(other instanceof VNode) || this.tag !== other.tag || this.key !== other.key || this.namespace !== other.namespace || this.childs.length !== other.childs.length) {
2838
+ if (!(other instanceof VNode) || !this.isSameKind(other) || this.childs.length !== other.childs.length) {
2831
2839
  return false;
2832
2840
  }
2833
2841
  if (this.attrs !== other.attrs) {
@@ -2902,23 +2910,20 @@ function morphNode(domNode, source, target, opts) {
2902
2910
  domNode.data = target.text;
2903
2911
  return domNode;
2904
2912
  }
2905
- if (type === 1 && source.tag === target.tag && source.namespace === target.namespace && source.key === target.key) {
2913
+ if (type === 1 && source.isSameKind(target)) {
2906
2914
  const propsDiff = diffProps(source.attrs, target.attrs);
2907
2915
  const isSelect = source.tag === "SELECT";
2908
2916
  if (propsDiff) {
2909
2917
  if (isSelect && "value" in propsDiff) {
2910
2918
  const { value: _v, ...rest } = propsDiff;
2911
2919
  applyProperties(domNode, rest, source.attrs);
2912
- } else {
2920
+ } else
2913
2921
  applyProperties(domNode, propsDiff, source.attrs);
2914
- }
2915
2922
  }
2916
- if (!target.attrs.dangerouslySetInnerHTML) {
2923
+ if (!target.attrs.dangerouslySetInnerHTML)
2917
2924
  morphChildren(domNode, source.childs, target.childs, opts);
2918
- }
2919
- if (isSelect && target.attrs.value !== undefined) {
2925
+ if (isSelect && target.attrs.value !== undefined)
2920
2926
  applyProperties(domNode, { value: target.attrs.value }, source.attrs);
2921
- }
2922
2927
  return domNode;
2923
2928
  }
2924
2929
  if (type === 11) {
@@ -2997,24 +3002,20 @@ function morphChildren(parentDom, oldChilds, newChilds, opts) {
2997
3002
  if (!used[i] && domNodes[i].parentNode === parentDom)
2998
3003
  parentDom.removeChild(domNodes[i]);
2999
3004
  }
3000
- var renderCache = new WeakMap;
3001
- function render(vnode, container, options) {
3002
- const cached = renderCache.get(container);
3005
+ function render(vnode, container, options, prev) {
3003
3006
  const isFragment = vnode instanceof VFragment;
3004
- if (cached && cached.vnode instanceof VFragment === isFragment) {
3005
- const oldDom = isFragment ? container : cached.dom;
3006
- const newDom = morphNode(oldDom, cached.vnode, vnode, options);
3007
- renderCache.set(container, { vnode, dom: isFragment ? container : newDom });
3008
- return newDom;
3007
+ if (prev && prev.vnode instanceof VFragment === isFragment) {
3008
+ const oldDom = isFragment ? container : prev.dom;
3009
+ const newDom = morphNode(oldDom, prev.vnode, vnode, options);
3010
+ return { vnode, dom: isFragment ? container : newDom };
3009
3011
  }
3010
- renderCache.delete(container);
3011
3012
  const domNode = vnode.toDom(options);
3012
3013
  container.replaceChildren(domNode);
3013
- renderCache.set(container, { vnode, dom: isFragment ? container : domNode });
3014
- return domNode;
3014
+ return { vnode, dom: isFragment ? container : domNode };
3015
3015
  }
3016
3016
  function h(tagName, properties, children) {
3017
- const tag = tagName.toUpperCase();
3017
+ const c = tagName.charCodeAt(0);
3018
+ const tag = c >= 97 && c <= 122 ? tagName.toUpperCase() : tagName;
3018
3019
  const props = {};
3019
3020
  let key, namespace;
3020
3021
  if (properties) {
@@ -3065,6 +3066,7 @@ class App {
3065
3066
  };
3066
3067
  this._compiled = false;
3067
3068
  this._renderOpts = { document: rootNode.ownerDocument };
3069
+ this._renderState = null;
3068
3070
  }
3069
3071
  get state() {
3070
3072
  return this.transactor.state;
@@ -3171,7 +3173,10 @@ class App {
3171
3173
  render() {
3172
3174
  const root = this.state.val;
3173
3175
  const stack = this.makeStack(root);
3174
- return render(this.renderer.renderRoot(stack, root), this.rootNode, this._renderOpts);
3176
+ const { renderer, rootNode, _renderOpts, _renderState } = this;
3177
+ const newState = render(renderer.renderRoot(stack, root), rootNode, _renderOpts, _renderState);
3178
+ this._renderState = newState;
3179
+ return newState.dom;
3175
3180
  }
3176
3181
  onChange(callback) {
3177
3182
  this.transactor.state.onChange(callback);
@@ -8093,7 +8098,7 @@ class Renderer {
8093
8098
  renderToDOM(stack, val) {
8094
8099
  const rootNode = document.createElement("div");
8095
8100
  const rOpts = { document };
8096
- render(h("div", null, [this.renderRoot(stack, val)]), rootNode, rOpts);
8101
+ render(h("DIV", null, [this.renderRoot(stack, val)]), rootNode, rOpts);
8097
8102
  return rootNode.childNodes[0];
8098
8103
  }
8099
8104
  renderToString(stack, val, cleanAttrs = true) {
@@ -8558,9 +8563,9 @@ class LintClassCollectorCtx extends ParseCtxClassSetCollector {
8558
8563
  this.attrs = [];
8559
8564
  }
8560
8565
  enterMacro(macroName, macroVars, macroSlots) {
8561
- const { DOMParser: DP, Text, Comment, nodes, events, macroNodes } = this;
8566
+ const { document: document2, Text, Comment, nodes, events, macroNodes } = this;
8562
8567
  const frame = { macroName, macroVars, macroSlots };
8563
- const v = new LintClassCollectorCtx(DP, Text, Comment, nodes, events, macroNodes, frame, this);
8568
+ const v = new LintClassCollectorCtx(document2, Text, Comment, nodes, events, macroNodes, frame, this);
8564
8569
  v.classes = this.classes;
8565
8570
  v.attrs = this.attrs;
8566
8571
  return v;