tutuca 0.9.27 → 0.9.28

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;
@@ -1442,26 +1438,28 @@ class ParseContext {
1442
1438
  onAttributes(_attrs, _wrapperAttrs, _textChild, _isMacroCall) {}
1443
1439
  }
1444
1440
  function condenseChildsWhites(childs) {
1445
- let end = childs.length;
1446
- if (end === 0)
1441
+ if (childs.length === 0)
1447
1442
  return childs;
1448
- let start = 0;
1449
1443
  let changed = false;
1450
- if (isTextNodeAllBlanks(childs[0])) {
1451
- start = 1;
1444
+ if (childs[0].isWhiteSpace?.()) {
1445
+ childs[0].condenseWhiteSpace();
1452
1446
  changed = true;
1453
1447
  }
1454
- if (end > 1 && isTextNodeAllBlanks(childs[end - 1])) {
1455
- end -= 1;
1448
+ const last = childs.length - 1;
1449
+ if (last > 0 && childs[last].isWhiteSpace?.()) {
1450
+ childs[last].condenseWhiteSpace();
1456
1451
  changed = true;
1457
1452
  }
1458
- for (let i = 1;i < end - 1; i++) {
1453
+ for (let i = 1;i < last; i++) {
1459
1454
  const cur = childs[i];
1460
- if (isTextNodeAllBlanks(cur) && isFirstDomNode(childs[i - 1]) && isFirstDomNode(childs[i + 1]) && cur.hasNewLine()) {
1461
- cur.condenseWhiteSpace();
1455
+ if (cur.isWhiteSpace?.() && cur.hasNewLine()) {
1456
+ const bothBlock = isBlockDomNode(childs[i - 1]) && isBlockDomNode(childs[i + 1]);
1457
+ cur.condenseWhiteSpace(bothBlock ? "" : " ");
1458
+ if (bothBlock)
1459
+ changed = true;
1462
1460
  }
1463
1461
  }
1464
- return changed ? childs.slice(start, end) : childs;
1462
+ return changed ? childs.filter((c) => !(c instanceof TextNode && c.val === "")) : childs;
1465
1463
  }
1466
1464
 
1467
1465
  class NodeEvents {
@@ -1517,7 +1515,10 @@ function compileModifiers(eventName, names) {
1517
1515
  return w(this, f, args, ctx);
1518
1516
  };
1519
1517
  }
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;
1518
+ 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, _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) => {
1519
+ const node = n instanceof FragmentNode ? n.childs[0] : n;
1520
+ return node instanceof DomNode && HTML_BLOCK_TAGS.has(node.tagName);
1521
+ }, 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
1522
  var init_anode = __esm(() => {
1522
1523
  init_attribute();
1523
1524
  init_path();
@@ -1546,8 +1547,8 @@ var init_anode = __esm(() => {
1546
1547
  }
1547
1548
  return false;
1548
1549
  }
1549
- condenseWhiteSpace() {
1550
- this.val = "";
1550
+ condenseWhiteSpace(replacement = "") {
1551
+ this.val = replacement;
1551
1552
  }
1552
1553
  isConstant() {
1553
1554
  return true;
@@ -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);
@@ -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,
@@ -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;
@@ -1058,7 +1058,7 @@ class ANode extends BaseNode {
1058
1058
  if (textChild)
1059
1059
  childs.unshift(new RenderTextNode(null, textChild));
1060
1060
  const domChilds = tag !== "PRE" ? condenseChildsWhites(childs) : childs;
1061
- return wrap(new DomNode(tag.toLowerCase(), nAttrs, domChilds), px, wrappers);
1061
+ return wrap(new DomNode(tag, nAttrs, domChilds), px, wrappers);
1062
1062
  }
1063
1063
  return new CommentNode(`Error: InvalidTagName ${tag}`);
1064
1064
  }
@@ -1365,29 +1365,35 @@ class ParseContext {
1365
1365
  }
1366
1366
  onAttributes(_attrs, _wrapperAttrs, _textChild, _isMacroCall) {}
1367
1367
  }
1368
- var isTextNodeAllBlanks = (n) => n instanceof TextNode && n.isWhiteSpace();
1369
- var isFirstDomNode = (n) => n instanceof DomNode || n instanceof FragmentNode && n.childs[0] instanceof DomNode;
1368
+ 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";
1369
+ var HTML_BLOCK_TAGS = new Set(_htmlBlockTags.split(","));
1370
+ var isBlockDomNode = (n) => {
1371
+ const node = n instanceof FragmentNode ? n.childs[0] : n;
1372
+ return node instanceof DomNode && HTML_BLOCK_TAGS.has(node.tagName);
1373
+ };
1370
1374
  function condenseChildsWhites(childs) {
1371
- let end = childs.length;
1372
- if (end === 0)
1375
+ if (childs.length === 0)
1373
1376
  return childs;
1374
- let start = 0;
1375
1377
  let changed = false;
1376
- if (isTextNodeAllBlanks(childs[0])) {
1377
- start = 1;
1378
+ if (childs[0].isWhiteSpace?.()) {
1379
+ childs[0].condenseWhiteSpace();
1378
1380
  changed = true;
1379
1381
  }
1380
- if (end > 1 && isTextNodeAllBlanks(childs[end - 1])) {
1381
- end -= 1;
1382
+ const last = childs.length - 1;
1383
+ if (last > 0 && childs[last].isWhiteSpace?.()) {
1384
+ childs[last].condenseWhiteSpace();
1382
1385
  changed = true;
1383
1386
  }
1384
- for (let i = 1;i < end - 1; i++) {
1387
+ for (let i = 1;i < last; i++) {
1385
1388
  const cur = childs[i];
1386
- if (isTextNodeAllBlanks(cur) && isFirstDomNode(childs[i - 1]) && isFirstDomNode(childs[i + 1]) && cur.hasNewLine()) {
1387
- cur.condenseWhiteSpace();
1389
+ if (cur.isWhiteSpace?.() && cur.hasNewLine()) {
1390
+ const bothBlock = isBlockDomNode(childs[i - 1]) && isBlockDomNode(childs[i + 1]);
1391
+ cur.condenseWhiteSpace(bothBlock ? "" : " ");
1392
+ if (bothBlock)
1393
+ changed = true;
1388
1394
  }
1389
1395
  }
1390
- return changed ? childs.slice(start, end) : childs;
1396
+ return changed ? childs.filter((c) => !(c instanceof TextNode && c.val === "")) : childs;
1391
1397
  }
1392
1398
 
1393
1399
  class View {
@@ -2824,10 +2830,13 @@ class VNode extends VBase {
2824
2830
  get nodeType() {
2825
2831
  return 1;
2826
2832
  }
2833
+ isSameKind(other) {
2834
+ return this.tag === other.tag && this.namespace === other.namespace && this.key === other.key;
2835
+ }
2827
2836
  isEqualTo(other) {
2828
2837
  if (this === other)
2829
2838
  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) {
2839
+ if (!(other instanceof VNode) || !this.isSameKind(other) || this.childs.length !== other.childs.length) {
2831
2840
  return false;
2832
2841
  }
2833
2842
  if (this.attrs !== other.attrs) {
@@ -2902,23 +2911,20 @@ function morphNode(domNode, source, target, opts) {
2902
2911
  domNode.data = target.text;
2903
2912
  return domNode;
2904
2913
  }
2905
- if (type === 1 && source.tag === target.tag && source.namespace === target.namespace && source.key === target.key) {
2914
+ if (type === 1 && source.isSameKind(target)) {
2906
2915
  const propsDiff = diffProps(source.attrs, target.attrs);
2907
2916
  const isSelect = source.tag === "SELECT";
2908
2917
  if (propsDiff) {
2909
2918
  if (isSelect && "value" in propsDiff) {
2910
2919
  const { value: _v, ...rest } = propsDiff;
2911
2920
  applyProperties(domNode, rest, source.attrs);
2912
- } else {
2921
+ } else
2913
2922
  applyProperties(domNode, propsDiff, source.attrs);
2914
- }
2915
2923
  }
2916
- if (!target.attrs.dangerouslySetInnerHTML) {
2924
+ if (!target.attrs.dangerouslySetInnerHTML)
2917
2925
  morphChildren(domNode, source.childs, target.childs, opts);
2918
- }
2919
- if (isSelect && target.attrs.value !== undefined) {
2926
+ if (isSelect && target.attrs.value !== undefined)
2920
2927
  applyProperties(domNode, { value: target.attrs.value }, source.attrs);
2921
- }
2922
2928
  return domNode;
2923
2929
  }
2924
2930
  if (type === 11) {
@@ -2997,24 +3003,20 @@ function morphChildren(parentDom, oldChilds, newChilds, opts) {
2997
3003
  if (!used[i] && domNodes[i].parentNode === parentDom)
2998
3004
  parentDom.removeChild(domNodes[i]);
2999
3005
  }
3000
- var renderCache = new WeakMap;
3001
- function render(vnode, container, options) {
3002
- const cached = renderCache.get(container);
3006
+ function render(vnode, container, options, prev) {
3003
3007
  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;
3008
+ if (prev && prev.vnode instanceof VFragment === isFragment) {
3009
+ const oldDom = isFragment ? container : prev.dom;
3010
+ const newDom = morphNode(oldDom, prev.vnode, vnode, options);
3011
+ return { vnode, dom: isFragment ? container : newDom };
3009
3012
  }
3010
- renderCache.delete(container);
3011
3013
  const domNode = vnode.toDom(options);
3012
3014
  container.replaceChildren(domNode);
3013
- renderCache.set(container, { vnode, dom: isFragment ? container : domNode });
3014
- return domNode;
3015
+ return { vnode, dom: isFragment ? container : domNode };
3015
3016
  }
3016
3017
  function h(tagName, properties, children) {
3017
- const tag = tagName.toUpperCase();
3018
+ const c = tagName.charCodeAt(0);
3019
+ const tag = c >= 97 && c <= 122 ? tagName.toUpperCase() : tagName;
3018
3020
  const props = {};
3019
3021
  let key, namespace;
3020
3022
  if (properties) {
@@ -3065,6 +3067,7 @@ class App {
3065
3067
  };
3066
3068
  this._compiled = false;
3067
3069
  this._renderOpts = { document: rootNode.ownerDocument };
3070
+ this._renderState = null;
3068
3071
  }
3069
3072
  get state() {
3070
3073
  return this.transactor.state;
@@ -3171,7 +3174,10 @@ class App {
3171
3174
  render() {
3172
3175
  const root = this.state.val;
3173
3176
  const stack = this.makeStack(root);
3174
- return render(this.renderer.renderRoot(stack, root), this.rootNode, this._renderOpts);
3177
+ const { renderer, rootNode, _renderOpts, _renderState } = this;
3178
+ const newState = render(renderer.renderRoot(stack, root), rootNode, _renderOpts, _renderState);
3179
+ this._renderState = newState;
3180
+ return newState.dom;
3175
3181
  }
3176
3182
  onChange(callback) {
3177
3183
  this.transactor.state.onChange(callback);
@@ -8093,7 +8099,7 @@ class Renderer {
8093
8099
  renderToDOM(stack, val) {
8094
8100
  const rootNode = document.createElement("div");
8095
8101
  const rOpts = { document };
8096
- render(h("div", null, [this.renderRoot(stack, val)]), rootNode, rOpts);
8102
+ render(h("DIV", null, [this.renderRoot(stack, val)]), rootNode, rOpts);
8097
8103
  return rootNode.childNodes[0];
8098
8104
  }
8099
8105
  renderToString(stack, val, cleanAttrs = true) {