@domphy/doctor 0.12.0 → 0.13.0

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.
@@ -1,5 +1,5 @@
1
- "use strict";var Domphy=(()=>{var f=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var p=(t,s)=>{for(var e in s)f(t,e,{get:s[e],enumerable:!0})},E=(t,s,e,h)=>{if(s&&typeof s=="object"||typeof s=="function")for(let r of S(s))!x.call(t,r)&&r!==e&&f(t,r,{get:()=>s[r],enumerable:!(h=k(s,r))||h.enumerable});return t};var R=t=>E(f({},"__esModule",{value:!0}),t);var L={};p(L,{doctor:()=>d});var d={};p(d,{diagnose:()=>b,format:()=>w});var C=["onAbort","onAuxClick","onBeforeMatch","onBeforeToggle","onBlur","onCancel","onCanPlay","onCanPlayThrough","onChange","onClick","onClose","onContextLost","onContextMenu","onContextRestored","onCopy","onCueChange","onCut","onDblClick","onDrag","onDragEnd","onDragEnter","onDragLeave","onDragOver","onDragStart","onDrop","onDurationChange","onEmptied","onEnded","onError","onFocus","onFormData","onInput","onInvalid","onKeyDown","onKeyPress","onKeyUp","onLoad","onLoadedData","onLoadedMetadata","onLoadStart","onMouseDown","onMouseEnter","onMouseLeave","onMouseMove","onMouseOut","onMouseOver","onMouseUp","onPaste","onPause","onPlay","onPlaying","onProgress","onRateChange","onReset","onResize","onScroll","onScrollEnd","onSecurityPolicyViolation","onSeeked","onSeeking","onSelect","onSlotChange","onStalled","onSubmit","onSuspend","onTimeUpdate","onToggle","onVolumeChange","onWaiting","onWheel","onTouchStart","onTouchMove","onTouchEnd","onTouchCancel","onPointerDown","onPointerMove","onPointerUp","onPointerCancel","onPointerEnter","onPointerLeave","onPointerOver","onPointerOut","onGotPointerCapture","onLostPointerCapture","onCompositionStart","onCompositionUpdate","onCompositionEnd","onTransitionEnd","onTransitionStart","onAnimationStart","onAnimationEnd","onAnimationIteration","onFullscreenChange","onFullscreenError","onFocusIn","onFocusOut"],B=C.reduce((t,s)=>{let e=s.slice(2).toLowerCase();return t[e]=s,t},{}),y=["a","abbr","address","article","aside","audio","b","base","blockquote","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","i","iframe","img","input","ins","kbd","label","legend","li","main","map","mark","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","slot","small","source","span","strong","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","title","tr","track","u","ul","var","video","wbr","bdi","bdo","math","menu","search","area","embed","hr","animate","animateMotion","animateTransform","circle","clipPath","cursor","defs","desc","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","prefetch","radialGradient","rect","set","solidColor","stop","svg","switch","symbol","tbreak","text","textPath","tspan","use","view"];var g=["area","base","br","col","embed","hr","img","input","link","meta","source","track","wbr"],_=["svg","circle","path","rect","ellipse","line","polyline","polygon","g","defs","use","symbol","linearGradient","radialGradient","stop","clipPath","mask","filter","text","tspan","textPath","image","pattern","marker","animate","animateTransform","animateMotion","feGaussianBlur","feComposite","feColorMatrix","feMerge","feMergeNode","feOffset","feFlood","feBlend","foreignObject"];var $=new Set([...y,..._]),T=new Set(g),j=new Set(["$","style","_key","_portal","_context","_metadata"]),M=new Set(["fontSize","lineHeight","fontWeight","letterSpacing"]);function m(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function v(t){for(let s in t)if($.has(s))return s}function b(t,s={}){let e=[];return c(t,"",e,!1,s.runReactive!==!1),e}function c(t,s,e,h,r){if(typeof t=="function"){if(!r)return;let n;try{n=t(()=>{})}catch(i){return}c(n,s,e,!0,r);return}if(Array.isArray(t)){if(h){let n=t.filter(i=>m(i)&&v(i));n.length>1&&n.some(i=>i._key===void 0)&&e.push({rule:"missing-key",severity:"warning",path:s||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."})}t.forEach((n,i)=>c(n,`${s}[${i}]`,e,!1,r));return}if(!m(t))return;let l=t,o=v(l),u=o?s?`${s} > ${o}`:o:s||"(root)";if(!o){let n=Object.keys(l).filter(i=>!j.has(i)&&!i.startsWith("_on")&&!i.startsWith("on")&&!i.startsWith("data")&&!i.startsWith("aria"));n.length===1&&e.push({rule:"unknown-tag",severity:"warning",path:u,message:`"${n[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let a=l[o];if(T.has(o)&&a!==null&&a!==void 0&&e.push({rule:"void-content",severity:"error",path:u,message:`Void tag "${o}" must have null content (got ${Array.isArray(a)?"array":typeof a}).`,hint:`Write { ${o}: null, \u2026 } and put attributes as sibling keys.`}),m(l.style)){let n=l.style;for(let i in n)M.has(i)&&typeof n[i]!="function"&&e.push({rule:"inline-typography",severity:"warning",path:u,message:`Inline \`${i}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."})}c(a,u,e,!1,r)}function w(t){if(t.length===0)return"\u2713 No issues found.";let s=e=>e==="error"?"\u2717":e==="warning"?"\u26A0":"i";return t.map(e=>`${s(e.severity)} [${e.rule}] ${e.path}
1
+ "use strict";var Domphy=(()=>{var y=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var $=Object.getOwnPropertyNames;var T=Object.prototype.hasOwnProperty;var b=(t,s)=>{for(var e in s)y(t,e,{get:s[e],enumerable:!0})},j=(t,s,e,a)=>{if(s&&typeof s=="object"||typeof s=="function")for(let r of $(s))!T.call(t,r)&&r!==e&&y(t,r,{get:()=>s[r],enumerable:!(a=C(s,r))||a.enumerable});return t};var L=t=>j(y({},"__esModule",{value:!0}),t);var A={};b(A,{doctor:()=>p});var p={};b(p,{diagnose:()=>_,format:()=>R,validate:()=>E});var M=["onAbort","onAuxClick","onBeforeMatch","onBeforeToggle","onBlur","onCancel","onCanPlay","onCanPlayThrough","onChange","onClick","onClose","onContextLost","onContextMenu","onContextRestored","onCopy","onCueChange","onCut","onDblClick","onDrag","onDragEnd","onDragEnter","onDragLeave","onDragOver","onDragStart","onDrop","onDurationChange","onEmptied","onEnded","onError","onFocus","onFormData","onInput","onInvalid","onKeyDown","onKeyPress","onKeyUp","onLoad","onLoadedData","onLoadedMetadata","onLoadStart","onMouseDown","onMouseEnter","onMouseLeave","onMouseMove","onMouseOut","onMouseOver","onMouseUp","onPaste","onPause","onPlay","onPlaying","onProgress","onRateChange","onReset","onResize","onScroll","onScrollEnd","onSecurityPolicyViolation","onSeeked","onSeeking","onSelect","onSlotChange","onStalled","onSubmit","onSuspend","onTimeUpdate","onToggle","onVolumeChange","onWaiting","onWheel","onTouchStart","onTouchMove","onTouchEnd","onTouchCancel","onPointerDown","onPointerMove","onPointerUp","onPointerCancel","onPointerEnter","onPointerLeave","onPointerOver","onPointerOut","onGotPointerCapture","onLostPointerCapture","onCompositionStart","onCompositionUpdate","onCompositionEnd","onTransitionEnd","onTransitionStart","onAnimationStart","onAnimationEnd","onAnimationIteration","onFullscreenChange","onFullscreenError","onFocusIn","onFocusOut"],I=M.reduce((t,s)=>{let e=s.slice(2).toLowerCase();return t[e]=s,t},{}),w=["a","abbr","address","article","aside","audio","b","base","blockquote","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","i","iframe","img","input","ins","kbd","label","legend","li","main","map","mark","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","slot","small","source","span","strong","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","title","tr","track","u","ul","var","video","wbr","bdi","bdo","math","menu","search","area","embed","hr","animate","animateMotion","animateTransform","circle","clipPath","cursor","defs","desc","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","prefetch","radialGradient","rect","set","solidColor","stop","svg","switch","symbol","tbreak","text","textPath","tspan","use","view"];var k=["area","base","br","col","embed","hr","img","input","link","meta","source","track","wbr"],S=["svg","circle","path","rect","ellipse","line","polyline","polygon","g","defs","use","symbol","linearGradient","radialGradient","stop","clipPath","mask","filter","text","tspan","textPath","image","pattern","marker","animate","animateTransform","animateMotion","feGaussianBlur","feComposite","feColorMatrix","feMerge","feMergeNode","feOffset","feFlood","feBlend","foreignObject"];var O=new Set([...w,...S]),B=new Set(k),N=new Set(["$","style","_key","_portal","_context","_metadata"]),D=new Set(["fontSize","lineHeight","fontWeight","letterSpacing"]);function g(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function x(t){for(let s in t)if(O.has(s))return s}function _(t,s={}){let e=[];return m(t,"",e,!1,s.runReactive!==!1),e}function m(t,s,e,a,r){var v;if(typeof t=="function"){if(!r)return;let i;try{i=t(()=>{})}catch(n){return}m(i,s,e,!0,r);return}if(Array.isArray(t)){let i=t.filter(o=>g(o)&&x(o));a&&(i.length>1&&i.some(o=>o._key===void 0)&&e.push({rule:"missing-key",severity:"warning",path:s||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."}),i.length>1&&i.every((o,l)=>o._key===l)&&e.push({rule:"unstable-key",severity:"warning",path:s||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let n=new Map;for(let o of i){let l=o._key;if(l==null)continue;let f=`${typeof l}:${String(l)}`;n.set(f,((v=n.get(f))!=null?v:0)+1)}for(let[o,l]of n)if(l>1){let f=o.slice(o.indexOf(":")+1);e.push({rule:"duplicate-key",severity:"error",path:s||"(list)",message:`Duplicate \`_key\` "${f}" among ${l} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}t.forEach((o,l)=>{m(o,`${s}[${l}]`,e,!1,r)});return}if(!g(t))return;let u=t,h=x(u),d=h?s?`${s} > ${h}`:h:s||"(root)";if(!h){let i=Object.keys(u).filter(n=>!N.has(n)&&!n.startsWith("_on")&&!n.startsWith("on")&&!n.startsWith("data")&&!n.startsWith("aria"));i.length===1&&e.push({rule:"unknown-tag",severity:"warning",path:d,message:`"${i[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let c=u[h];if(B.has(h)&&c!==null&&c!==void 0&&e.push({rule:"void-content",severity:"error",path:d,message:`Void tag "${h}" must have null content (got ${Array.isArray(c)?"array":typeof c}).`,hint:`Write { ${h}: null, \u2026 } and put attributes as sibling keys.`}),g(u.style)){let i=u.style;for(let n in i)D.has(n)&&typeof i[n]!="function"&&e.push({rule:"inline-typography",severity:"warning",path:d,message:`Inline \`${n}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."})}m(c,d,e,!1,r)}function E(t,s={}){let e=_(t,s),a={error:0,warning:0,info:0,total:e.length};for(let r of e)a[r.severity]+=1;return{ok:a.error===0,issues:e,summary:a}}function R(t){if(t.length===0)return"\u2713 No issues found.";let s=e=>e==="error"?"\u2717":e==="warning"?"\u26A0":"i";return t.map(e=>`${s(e.severity)} [${e.rule}] ${e.path}
2
2
  ${e.message}${e.hint?`
3
3
  \u2192 ${e.hint}`:""}`).join(`
4
- `)}return R(L);})();
4
+ `)}return L(A);})();
5
5
  //# sourceMappingURL=doctor.global.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/global.ts","../src/index.ts","../../core/src/types/EventProperties.ts","../../core/src/constants/HtmlTags.ts","../../core/src/classes/Notifier.ts","../../core/src/classes/State.ts","../../core/src/utils.ts","../../core/src/helpers.ts","../../core/src/constants/VoidTags.ts","../../core/src/constants/SvgTags.ts","../../core/src/constants/BooleanAttributes.ts","../../core/src/constants/PrefixCSS.ts","../../core/src/constants/CamelAttributes.ts","../../core/src/classes/ElementAttribute.ts","../../core/src/classes/AttributeList.ts","../../core/src/classes/TextNode.ts","../../core/src/classes/ElementList.ts","../../core/src/classes/StyleProperty.ts","../../core/src/classes/StyleRule.ts","../../core/src/classes/StyleList.ts","../../core/src/classes/ElementNode.ts","../../core/src/classes/RecordState.ts","../src/diagnose.ts"],"sourcesContent":["export * as doctor from \"./index\";\n","// @domphy/doctor — static analyzer for Domphy element trees. Catches\n// non-idiomatic patterns (inline typography, void-tag content, missing _key on\n// dynamic lists, unknown tags) so humans and AI agents get a feedback loop to\n// self-correct generated code.\n\nexport type { DiagnoseOptions, Diagnostic, Severity } from \"./diagnose.js\";\nexport { diagnose, format } from \"./diagnose.js\";\n","export const EventProperties = [\r\n \"onAbort\",\r\n \"onAuxClick\",\r\n \"onBeforeMatch\",\r\n \"onBeforeToggle\",\r\n \"onBlur\",\r\n \"onCancel\",\r\n \"onCanPlay\",\r\n \"onCanPlayThrough\",\r\n \"onChange\",\r\n \"onClick\",\r\n \"onClose\",\r\n \"onContextLost\",\r\n \"onContextMenu\",\r\n \"onContextRestored\",\r\n \"onCopy\",\r\n \"onCueChange\",\r\n \"onCut\",\r\n \"onDblClick\",\r\n \"onDrag\",\r\n \"onDragEnd\",\r\n \"onDragEnter\",\r\n \"onDragLeave\",\r\n \"onDragOver\",\r\n \"onDragStart\",\r\n \"onDrop\",\r\n \"onDurationChange\",\r\n \"onEmptied\",\r\n \"onEnded\",\r\n \"onError\",\r\n \"onFocus\",\r\n \"onFormData\",\r\n \"onInput\",\r\n \"onInvalid\",\r\n \"onKeyDown\",\r\n \"onKeyPress\",\r\n \"onKeyUp\",\r\n \"onLoad\",\r\n \"onLoadedData\",\r\n \"onLoadedMetadata\",\r\n \"onLoadStart\",\r\n \"onMouseDown\",\r\n \"onMouseEnter\",\r\n \"onMouseLeave\",\r\n \"onMouseMove\",\r\n \"onMouseOut\",\r\n \"onMouseOver\",\r\n \"onMouseUp\",\r\n \"onPaste\",\r\n \"onPause\",\r\n \"onPlay\",\r\n \"onPlaying\",\r\n \"onProgress\",\r\n \"onRateChange\",\r\n \"onReset\",\r\n \"onResize\",\r\n \"onScroll\",\r\n \"onScrollEnd\",\r\n \"onSecurityPolicyViolation\",\r\n \"onSeeked\",\r\n \"onSeeking\",\r\n \"onSelect\",\r\n \"onSlotChange\",\r\n \"onStalled\",\r\n \"onSubmit\",\r\n \"onSuspend\",\r\n \"onTimeUpdate\",\r\n \"onToggle\",\r\n \"onVolumeChange\",\r\n \"onWaiting\",\r\n \"onWheel\",\r\n \"onTouchStart\",\r\n \"onTouchMove\",\r\n \"onTouchEnd\",\r\n \"onTouchCancel\",\r\n \"onPointerDown\",\r\n \"onPointerMove\",\r\n \"onPointerUp\",\r\n \"onPointerCancel\",\r\n \"onPointerEnter\",\r\n \"onPointerLeave\",\r\n \"onPointerOver\",\r\n \"onPointerOut\",\r\n \"onGotPointerCapture\",\r\n \"onLostPointerCapture\",\r\n \"onCompositionStart\",\r\n \"onCompositionUpdate\",\r\n \"onCompositionEnd\",\r\n \"onTransitionEnd\",\r\n \"onTransitionStart\",\r\n \"onAnimationStart\",\r\n \"onAnimationEnd\",\r\n \"onAnimationIteration\",\r\n \"onFullscreenChange\",\r\n \"onFullscreenError\",\r\n \"onFocusIn\",\r\n \"onFocusOut\",\r\n] as const\r\n\r\nexport const eventNameMap = EventProperties.reduce((acc, ev) => {\r\n const key = ev.slice(2).toLowerCase() as keyof HTMLElementEventMap\r\n acc[key] = ev;\r\n return acc;\r\n}, {} as Partial<Record<keyof HTMLElementEventMap, (typeof EventProperties)[number]>>);\r\n","export const HtmlTags = [\r\n \"a\",\r\n \"abbr\",\r\n \"address\",\r\n \"article\",\r\n \"aside\",\r\n \"audio\",\r\n \"b\",\r\n \"base\",\r\n \"blockquote\",\r\n \"br\",\r\n \"button\",\r\n \"canvas\",\r\n \"caption\",\r\n \"cite\",\r\n \"code\",\r\n \"col\",\r\n \"colgroup\",\r\n \"data\",\r\n \"datalist\",\r\n \"dd\",\r\n \"del\",\r\n \"details\",\r\n \"dfn\",\r\n \"dialog\",\r\n \"div\",\r\n \"dl\",\r\n \"dt\",\r\n \"em\",\r\n \"fieldset\",\r\n \"figcaption\",\r\n \"figure\",\r\n \"footer\",\r\n \"form\",\r\n \"h1\",\r\n \"h2\",\r\n \"h3\",\r\n \"h4\",\r\n \"h5\",\r\n \"h6\",\r\n \"header\",\r\n \"hgroup\",\r\n \"i\",\r\n \"iframe\",\r\n \"img\",\r\n \"input\",\r\n \"ins\",\r\n \"kbd\",\r\n \"label\",\r\n \"legend\",\r\n \"li\",\r\n \"main\",\r\n \"map\",\r\n \"mark\",\r\n \"meta\",\r\n \"meter\",\r\n \"nav\",\r\n \"noscript\",\r\n \"object\",\r\n \"ol\",\r\n \"optgroup\",\r\n \"option\",\r\n \"output\",\r\n \"p\",\r\n \"param\",\r\n \"picture\",\r\n \"pre\",\r\n \"progress\",\r\n \"q\",\r\n \"rp\",\r\n \"rt\",\r\n \"ruby\",\r\n \"s\",\r\n \"samp\",\r\n \"section\",\r\n \"select\",\r\n \"slot\",\r\n \"small\",\r\n \"source\",\r\n \"span\",\r\n \"strong\",\r\n \"sub\",\r\n \"summary\",\r\n \"sup\",\r\n \"table\",\r\n \"tbody\",\r\n \"td\",\r\n \"template\",\r\n \"textarea\",\r\n \"tfoot\",\r\n \"th\",\r\n \"thead\",\r\n \"time\",\r\n \"title\",\r\n \"tr\",\r\n \"track\",\r\n \"u\",\r\n \"ul\",\r\n \"var\",\r\n \"video\",\r\n \"wbr\",\r\n \"bdi\",\r\n \"bdo\",\r\n \"math\",\r\n \"menu\",\r\n \"search\",\r\n \"area\",\r\n \"embed\",\r\n \"hr\",\r\n \"animate\",\r\n \"animateMotion\",\r\n \"animateTransform\",\r\n \"circle\",\r\n \"clipPath\",\r\n \"cursor\",\r\n \"defs\",\r\n \"desc\",\r\n \"ellipse\",\r\n \"feBlend\",\r\n \"feColorMatrix\",\r\n \"feComponentTransfer\",\r\n \"feComposite\",\r\n \"feConvolveMatrix\",\r\n \"feDiffuseLighting\",\r\n \"feDisplacementMap\",\r\n \"feDistantLight\",\r\n \"feDropShadow\",\r\n \"feFlood\",\r\n \"feFuncA\",\r\n \"feFuncB\",\r\n \"feFuncG\",\r\n \"feFuncR\",\r\n \"feGaussianBlur\",\r\n \"feImage\",\r\n \"feMerge\",\r\n \"feMergeNode\",\r\n \"feMorphology\",\r\n \"feOffset\",\r\n \"fePointLight\",\r\n \"feSpecularLighting\",\r\n \"feSpotLight\",\r\n \"feTile\",\r\n \"feTurbulence\",\r\n \"filter\",\r\n \"foreignObject\",\r\n \"g\",\r\n \"image\",\r\n \"line\",\r\n \"linearGradient\",\r\n \"marker\",\r\n \"mask\",\r\n \"metadata\",\r\n \"mpath\",\r\n \"path\",\r\n \"pattern\",\r\n \"polygon\",\r\n \"polyline\",\r\n \"prefetch\",\r\n \"radialGradient\",\r\n \"rect\",\r\n \"set\",\r\n \"solidColor\",\r\n \"stop\",\r\n \"svg\",\r\n \"switch\",\r\n \"symbol\",\r\n \"tbreak\",\r\n \"text\",\r\n \"textPath\",\r\n \"tspan\",\r\n \"use\",\r\n \"view\",\r\n];","import { Handler } from \"../types.js\"\n\ntype ChainEntry = [notifier: Notifier, event: string]\n\n// Shared across all instances to track the flush chain for circular detection.\nlet _chain: ChainEntry[] = []\n\n// Microtask scheduler. Older embedded Chromium runtimes (SketchUp 2020 /\n// 2021.0 ship CEF 64) predate `queueMicrotask` (added in Chrome 71). A\n// resolved Promise's `.then` runs as a microtask in the same checkpoint, so\n// it is the standard fallback. The `.catch` mimics `queueMicrotask`'s\n// behaviour of surfacing thrown errors to the global error handler rather\n// than silently becoming an unhandled-rejection.\nconst _microtask: (cb: () => void) => void =\n typeof queueMicrotask === \"function\"\n ? queueMicrotask\n : (cb) => {\n Promise.resolve().then(cb).catch((e) => {\n setTimeout(() => { throw e }, 0)\n })\n }\n\n// Cap on self-re-notifications within one settle burst. A converging update\n// (clamp/normalize) reaches a fixpoint in a pass or two; anything beyond this is\n// a genuinely diverging self-feedback loop and is stopped like a cycle.\nconst SELF_NOTIFY_CAP = 100\n\nexport class Notifier {\n private _listeners: Record<string, Set<Handler>> | null = {}\n private _pending: Map<string, { args: unknown[], chain: ChainEntry[] }> = new Map()\n private _scheduled = false\n // Args currently being delivered per event (used to detect a self-update fixpoint).\n private _flushing: Map<string, unknown[]> = new Map()\n // Self-re-notification depth in the current settle burst (runaway guard).\n private _selfDepth = 0\n\n _dispose(): void {\n if (this._listeners) {\n for (const event in this._listeners) {\n this._listeners[event].clear()\n }\n }\n this._listeners = null\n }\n\n addListener(event: string, listener: Handler): () => void {\n if (!this._listeners) return () => {}\n\n if (typeof event !== \"string\" || typeof listener !== \"function\") {\n throw new Error(\"Event name must be a string, listener must be a function\")\n }\n\n if (!this._listeners[event]) {\n this._listeners[event] = new Set()\n }\n\n const release = () => this.removeListener(event, listener)\n\n if (this._listeners[event].has(listener)) return release\n\n this._listeners[event].add(listener)\n if (typeof listener.onSubscribe === \"function\") {\n listener.onSubscribe(release)\n }\n\n return release\n }\n\n removeListener(event: string, listener: Handler): void {\n if (!this._listeners) return\n\n const listeners = this._listeners[event]\n if (listeners && listeners.has(listener)) {\n listeners.delete(listener)\n if (listeners.size === 0) {\n delete this._listeners[event]\n }\n }\n }\n\n notify(event: string, ...args: unknown[]): void {\n if (!this._listeners) return\n if (!this._listeners[event]) return\n\n // A listener that re-sets its OWN state mid-flush shows up as [this,event] at\n // the TOP of the chain. That is a converging self-update (clamp/normalize),\n // not a cross-state cycle — let it re-propagate with a fresh chain. A deeper\n // match (intervening notifiers) is a real cycle and is still rejected.\n const top = _chain.length ? _chain[_chain.length - 1] : null\n const selfReentry = !!top && top[0] === this && top[1] === event\n\n if (selfReentry) {\n const inflight = this._flushing.get(event)\n // Same value as the one being delivered → fixpoint reached, stop quietly.\n if (inflight && inflight[0] === args[0]) return\n if (this._selfDepth >= SELF_NOTIFY_CAP) {\n console.error(`[Domphy] Runaway self-update on \"${event}\" — stopped after ${SELF_NOTIFY_CAP} iterations`)\n return\n }\n this._selfDepth++\n this._pending.set(event, { args, chain: [] })\n } else {\n if (this._isCircular(event)) return\n this._pending.set(event, { args, chain: [..._chain] })\n }\n\n if (!this._scheduled) {\n this._scheduled = true\n _microtask(() => this._flushAll())\n }\n }\n\n private _isCircular(event: string): boolean {\n const idx = _chain.findIndex(([n, e]) => n === this && e === event)\n if (idx === -1) return false\n\n const names = [..._chain.slice(idx).map(([, e]) => e), event]\n console.error(`[Domphy] Circular dependency detected:\\n ${names.join(\" → \")}`)\n return true\n }\n\n private _flushAll(): void {\n this._scheduled = false\n const pending = this._pending\n this._pending = new Map()\n\n for (const [event, { args, chain }] of pending) {\n _chain = chain\n this._flush(event, args)\n }\n _chain = []\n // Burst settled (no self-update re-queued anything) → reset the runaway guard.\n if (this._pending.size === 0) this._selfDepth = 0\n }\n\n private _flush(event: string, args: unknown[]): void {\n if (!this._listeners) return\n const listeners = this._listeners[event]\n if (!listeners) return\n\n _chain.push([this, event])\n this._flushing.set(event, args)\n\n for (const listener of [...listeners]) {\n if (!listeners.has(listener)) continue\n try {\n listener(...args)\n } catch (e) {\n console.error(e)\n }\n }\n\n this._flushing.delete(event)\n _chain.pop()\n }\n}\n","import { Notifier } from \"./Notifier.js\";\r\nimport { Handler } from \"../types.js\"\r\n\r\nexport type ValueListener<T> = ((_value: T) => void) & Handler\r\nexport type ValueOrState<T> = T | State<T>\r\n\r\nexport class State<T> {\r\n readonly _isState = true;\r\n private _value: T;\r\n readonly initialValue: T;\r\n private _notifier: Notifier | null = new Notifier();\r\n\r\n constructor(initialValue: T, readonly name: string = typeof initialValue) {\r\n this.initialValue = initialValue;\r\n this._value = initialValue;\r\n }\r\n\r\n get(listener?: ValueListener<T>): T {\r\n if (listener) this.addListener(listener);\r\n return this._value;\r\n }\r\n\r\n set(newValue: T): void {\r\n if (!this._notifier) return;\r\n this._value = newValue;\r\n this._notifier.notify(this.name, newValue);\r\n }\r\n\r\n reset(): void {\r\n this.set(this.initialValue);\r\n }\r\n\r\n addListener(listener: ValueListener<T>): () => void {\r\n if (!this._notifier) return () => { };\r\n return this._notifier.addListener(this.name, listener);\r\n }\r\n\r\n removeListener(listener: ValueListener<T>): void {\r\n if (!this._notifier) return;\r\n this._notifier.removeListener(this.name, listener);\r\n }\r\n\r\n _dispose(): void {\r\n if (this._notifier) {\r\n this._notifier._dispose();\r\n this._notifier = null;\r\n }\r\n }\r\n}\r\n","import { DomphyElement, HookMap, EventName, Handler, Listener } from \"./types.js\";\r\nimport { State } from \"./classes/State.js\"\r\n\r\nimport { deepClone, addEvent, addHook } from \"./helpers.js\"\r\n\r\nexport function merge(source: Record<string, any> = {}, target: Record<string, any> = {}): Record<string, any> {\r\n const comma = [\"animation\", \"transition\", \"boxShadow\", \"textShadow\", \"background\", \"fontFamily\"]\r\n const space = [\"class\", \"rel\", \"transform\", \"acceptCharset\", \"sandbox\"]\r\n const adjacent = [\"content\"]\r\n if (Object.prototype.toString.call(target) === \"[object Object]\" && Object.getPrototypeOf(target) === Object.prototype) { // plainjs not class instance\r\n target = deepClone(target)\r\n }\r\n\r\n for (const key in target) {\r\n\r\n const value = target[key];\r\n if (value === undefined || value === null || value === \"\") continue;\r\n\r\n if (typeof value === \"object\" && !Array.isArray(value)) {\r\n if (typeof source[key] === \"object\") {\r\n source[key] = merge(source[key], value);\r\n } else {\r\n source[key] = value;\r\n }\r\n\r\n } else {\r\n if (comma.includes(key)) {\r\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\r\n let old = source[key]\r\n source[key] = (listener: Handler) => {\r\n let val1 = typeof old === \"function\" ? old(listener) : old\r\n let val2 = typeof value === \"function\" ? value(listener) : value\r\n return [val1, val2].filter(e => e).join(\", \")\r\n }\r\n } else {\r\n source[key] = [source[key], value].filter(e => e).join(\", \")\r\n }\r\n\r\n } else if (adjacent.includes(key)) {\r\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\r\n let old = source[key]\r\n source[key] = (listener: Handler) => {\r\n let val1 = typeof old === \"function\" ? old(listener) : old\r\n let val2 = typeof value === \"function\" ? value(listener) : value\r\n return [val1, val2].filter(e => e).join(\"\")\r\n }\r\n } else {\r\n source[key] = [source[key], value].filter(e => e).join(\"\")\r\n }\r\n } else if (space.includes(key)) {\r\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\r\n let old = source[key]\r\n source[key] = (listener: Handler) => {\r\n let val1 = typeof old === \"function\" ? old(listener) : old\r\n let val2 = typeof value === \"function\" ? value(listener) : value\r\n return [val1, val2].filter(e => e).join(\" \")\r\n }\r\n } else {\r\n source[key] = [source[key], value].filter(e => e).join(\" \")\r\n }\r\n } else if (key.startsWith(\"on\")) {\r\n let name = key.replace(\"on\", \"\").toLowerCase() as EventName\r\n addEvent(source as DomphyElement, name, value)\r\n } else if (key.startsWith(\"_on\")) {\r\n let name = key.replace(\"_on\", \"\") as keyof HookMap\r\n addHook(source as DomphyElement, name, value)\r\n } else {\r\n source[key] = value;\r\n }\r\n }\r\n }\r\n return source;\r\n}\r\n\r\nexport function hashString(str: string = \"\"): string {\r\n let hash = 0x811c9dc5; // FNV-1a 32-bit offset basis\r\n for (let i = 0; i < str.length; i++) {\r\n hash ^= str.charCodeAt(i);\r\n hash = (hash * 0x01000193) >>> 0; // FNV prime, keep 32-bit unsigned\r\n }\r\n return String.fromCharCode(97 + (hash % 26)) + hash.toString(16);\r\n}\r\n\r\nexport function toState<T>(val: T | State<T>, name?: string): State<T> {\r\n return (val instanceof State || (val as any)?._isState) ? val as State<T> : new State<T>(val, name);\r\n}\r\n\r\nexport function r<T>(fn: (listener: Listener) => T): (listener: Listener) => T {\r\n return fn;\r\n}\r\n\r\n","import { DomphyElement, PartialElement, HookMap, EventName, Handler, TagName } from \"./types.js\";\r\nimport { ElementNode } from \"./classes/ElementNode.js\"\r\nimport { State } from \"./classes/State.js\"\r\nimport { eventNameMap } from \"./types/EventProperties.js\"\r\nimport { HtmlTags } from \"./constants/HtmlTags.js\"\r\nimport {merge} from \"./utils.js\"\r\n\r\nexport function addHook<K extends keyof HookMap>(partial: PartialElement, hookName: K, handler: HookMap[K]): void {\r\n const hookProperty = `_on${hookName}` as keyof PartialElement;\r\n let current = partial[hookProperty];\r\n\r\n if (typeof current === \"function\") {\r\n (partial as any)[hookProperty] = (...args: any[]) => {\r\n (current as Function)(...args);\r\n (handler as Function)(...args);\r\n };\r\n } else {\r\n (partial as any)[hookProperty] = handler;\r\n }\r\n}\r\n\r\nexport function addEvent<K extends keyof HTMLElementEventMap>(\r\n attributes: PartialElement,\r\n eventName: K,\r\n handler: (event: HTMLElementEventMap[K], node: ElementNode) => void\r\n): void {\r\n const eventProperty = eventNameMap[eventName];\r\n if (!eventProperty) {\r\n throw Error(`invalid event name \"${eventName}\"`);\r\n }\r\n const current = (attributes as any)[eventProperty]\r\n\r\n if (typeof current == \"function\") {\r\n (attributes as any)[eventProperty] = (event: HTMLElementEventMap[K], node: ElementNode) => {\r\n current(event, node)\r\n handler(event, node);\r\n };\r\n } else {\r\n (attributes as any)[eventProperty] = handler\r\n }\r\n}\r\n\r\nexport function deepClone(value: any, seen = new WeakMap()): any {\r\n if (value === null || typeof value !== \"object\") return value;\r\n if (typeof value === \"function\") return value;\r\n if (seen.has(value)) return seen.get(value);\r\n\r\n const proto = Object.getPrototypeOf(value);\r\n if (proto !== Object.prototype && !Array.isArray(value)) return value; // ignore class instance\r\n\r\n let clone: any;\r\n\r\n if (Array.isArray(value)) {\r\n clone = [];\r\n seen.set(value, clone);\r\n for (const v of value) clone.push(deepClone(v, seen));\r\n return clone;\r\n }\r\n\r\n if (value instanceof Date) return new Date(value);\r\n if (value instanceof RegExp) return new RegExp(value);\r\n if (value instanceof Map) {\r\n clone = new Map();\r\n seen.set(value, clone);\r\n for (const [k, v] of value) clone.set(deepClone(k, seen), deepClone(v, seen));\r\n return clone;\r\n }\r\n if (value instanceof Set) {\r\n clone = new Set();\r\n seen.set(value, clone);\r\n for (const v of value) clone.add(deepClone(v, seen));\r\n return clone;\r\n }\r\n if (ArrayBuffer.isView(value)) {\r\n return new (value as any).constructor(value);\r\n }\r\n if (value instanceof ArrayBuffer) {\r\n return value.slice(0);\r\n }\r\n\r\n clone = Object.create(proto);\r\n seen.set(value, clone);\r\n\r\n for (const key of Reflect.ownKeys(value)) {\r\n clone[key] = deepClone(value[key], seen);\r\n }\r\n\r\n return clone;\r\n}\r\n\r\nexport function validate(element: DomphyElement | PartialElement, asPartial = false): boolean {\r\n if (Object.prototype.toString.call(element) !== \"[object Object]\") {\r\n throw Error(`typeof ${element} is invalid DomphyElement`);\r\n }\r\n let keys = Object.keys(element);\r\n for (let i = 0; i < keys.length; i++) {\r\n let key = keys[i];\r\n let val = element[key as keyof typeof element]\r\n if (i == 0 && !HtmlTags.includes(key) && !key.includes(\"-\") && !asPartial) { // web-component\r\n throw Error(`key ${key} is not valid HTML tag name`);\r\n } else if (key == \"style\" && val && Object.prototype.toString.call(val) !== \"[object Object]\") {\r\n throw Error(`\"style\" must be a object`);\r\n } else if (key == \"$\") {\r\n element.$!.forEach(v => validate(v as PartialElement, true))\r\n } else if (key.startsWith(\"_on\") && typeof val != \"function\") {\r\n throw Error(`hook ${key} value \"${val}\" must be a function `)\r\n } else if (key.startsWith(\"on\") && typeof val != \"function\") {\r\n throw Error(`event ${key} value \"${val}\" must be a function `);\r\n } else if (key == \"_portal\" && typeof val !== \"function\") {\r\n throw Error(`\"_portal\" must be a function return HTMLElement`);\r\n } else if (key == \"_context\" && Object.prototype.toString.call(val) !== \"[object Object]\") {\r\n throw Error(`\"_context\" must be a object`);\r\n } else if (key == \"_metadata\" && Object.prototype.toString.call(val) !== \"[object Object]\") {\r\n throw Error(`\"_metadata\" must be a object`);\r\n } else if (key == \"_key\" && (typeof val !== \"string\" && typeof val !== \"number\")) {\r\n throw Error(`\"_key\" must be a string or number`);\r\n }\r\n }\r\n return true;\r\n}\r\n\r\nexport function isValid(element: DomphyElement): boolean {\r\n\r\n if (Array.isArray(element)) return false;\r\n if (!element || typeof element !== \"object\") return false;\r\n\r\n let keys = Object.keys(element);\r\n for (let i = 0; i < keys.length; i++) {\r\n let key = keys[i];\r\n let val = element[key as keyof typeof element];\r\n if (i == 0 && !HtmlTags.includes(key)) return false\r\n if (key === \"style\" && (val == null || typeof val !== \"object\" || Array.isArray(val))) return false;\r\n if (key.startsWith(\"_on\") && typeof val !== \"function\") return false;\r\n if (key.startsWith(\"on\") && typeof val !== \"function\") return false;\r\n if (key === \"_portalChildren\" && !Array.isArray(val)) return false;\r\n if ((key === \"_context\" || key === \"_metadata\") && (val == null || typeof val !== \"object\" || Array.isArray(val))) return false;\r\n }\r\n return true;\r\n}\r\n\r\nexport function isHTML(str: string): boolean {\r\n return /<([a-z][\\w-]*)(\\s[^>]*)?>.*<\\/\\1>|<([a-z][\\w-]*)(\\s[^>]*)?\\/>/i.test(str.trim());\r\n}\r\n\r\nexport function escapeHTML(str: string): string {\r\n return str\r\n .replace(/&/g, \"&amp;\")\r\n .replace(/</g, \"&lt;\")\r\n .replace(/>/g, \"&gt;\")\r\n .replace(/\"/g, \"&quot;\")\r\n .replace(/'/g, \"&#39;\");\r\n}\r\n\r\nexport function addClass(element: PartialElement, className: string): void {\r\n\r\n if (typeof element.class == \"function\") {\r\n let reactive = element.class\r\n element.class = (listener) => String(reactive(listener)) + \" \" + className\r\n } else {\r\n let current = element.class || \"\"\r\n let split = String(current).split(\" \")\r\n split.push(className)\r\n element.class = split.filter(e => e).join(\" \")\r\n }\r\n\r\n}\r\n\r\nexport function removeClass(element: PartialElement, className: string): void {\r\n\r\n if (typeof element.class == \"function\") {\r\n let reactive = element.class\r\n element.class = (listener) => {\r\n let split = String(reactive(listener)).split(\" \")\r\n return split.filter(e => e != className).join(\" \")\r\n }\r\n } else {\r\n let split = String(element.class).split(\" \")\r\n element.class ||= \"\"\r\n element.class = split.filter(e => e != className).join(\" \")\r\n }\r\n\r\n}\r\n\r\nexport function toggleClass(element: PartialElement, className: string): void {\r\n\r\n if (typeof element.class == \"function\") {\r\n let reactive = element.class\r\n element.class = (listener) => {\r\n let split = String(reactive(listener)).split(\" \")\r\n return split.includes(className) ? split.filter(e => e != className).join(\" \") : split.concat([className]).join(\" \")\r\n }\r\n } else {\r\n let split = String(element.class).split(\" \")\r\n element.class ||= \"\"\r\n element.class = split.includes(className) ? split.filter(e => e != className).join(\" \") : split.concat([className]).join(\" \")\r\n }\r\n\r\n}\r\n\r\nexport function getTagName(element: DomphyElement): TagName | undefined {\r\n return Object.keys(element).find(e => HtmlTags.includes(e)) as TagName | undefined\r\n}\r\n\r\n\r\nexport function camelToKebab(str: string): string {\r\n return str.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase();\r\n}\r\n\r\nexport function selectorSplitter(selectors: string) {\r\n if (selectors.indexOf('@') === 0) {\r\n return [selectors];\r\n }\r\n var splitted = [];\r\n var parens = 0;\r\n var angulars = 0;\r\n var soFar = '';\r\n for (var i = 0, len = selectors.length; i < len; i++) {\r\n var char = selectors[i];\r\n if (char === '(') {\r\n parens += 1;\r\n } else if (char === ')') {\r\n parens -= 1;\r\n } else if (char === '[') {\r\n angulars += 1;\r\n } else if (char === ']') {\r\n angulars -= 1;\r\n } else if (char === ',') {\r\n if (!parens && !angulars) {\r\n splitted.push(soFar.trim());\r\n soFar = '';\r\n continue;\r\n }\r\n }\r\n soFar += char;\r\n }\r\n splitted.push(soFar.trim());\r\n return splitted;\r\n};\r\n\r\nexport function normalizeSelectorKey(selectorText: string): string {\r\n const text = selectorText.trim();\r\n // At-rule headers (@media, @keyframes, @supports...) are matched\r\n // whitespace-insensitive because CSSOM reformats them unpredictably.\r\n if (text.startsWith(\"@\")) return text.replace(/\\s+/g, \"\");\r\n return text\r\n .replace(/\\s*([>+~,])\\s*/g, \"$1\") // tighten combinators and selector lists\r\n .replace(/\\s+/g, \" \") // collapse descendant-combinator whitespace\r\n .replace(/\\(\\s*odd\\s*\\)/g, \"(2n+1)\") // CSSOM serializes :nth-child(odd) as (2n+1)\r\n .replace(/\\(\\s*even\\s*\\)/g, \"(2n)\")\r\n .trim();\r\n}\r\n\r\nexport function collectCSSRules(rules: CSSRuleList, map: Map<string, CSSRule>): Map<string, CSSRule> {\r\n for (let i = 0; i < rules.length; i++) {\r\n const rule = rules[i] as any;\r\n let key: string | null = null;\r\n if (typeof rule.selectorText === \"string\") {\r\n key = normalizeSelectorKey(rule.selectorText);\r\n } else if (typeof rule.cssText === \"string\" && rule.cssText.startsWith(\"@\")) {\r\n key = normalizeSelectorKey(rule.cssText.split(\"{\")[0]);\r\n }\r\n if (key && !map.has(key)) map.set(key, rule as CSSRule);\r\n }\r\n return map;\r\n}\r\n\r\nexport function ensureDomStyle(styleParent: HTMLHeadElement | ShadowRoot): HTMLStyleElement {\r\n let domStyle = styleParent.querySelector(\"#domphy-style\") as HTMLStyleElement | null;\r\n\r\n if (!domStyle) {\r\n domStyle = document.createElement(\"style\");\r\n domStyle.id = \"domphy-style\";\r\n styleParent.appendChild(domStyle);\r\n }\r\n\r\n if (domStyle.dataset.domphyBase !== \"true\") {\r\n domStyle.sheet?.insertRule(\"[hidden] { display: none !important; }\", 0);\r\n domStyle.dataset.domphyBase = \"true\";\r\n }\r\n\r\n return domStyle;\r\n}\r\n\r\nexport const mergePartial = (partial: PartialElement | DomphyElement): typeof partial => {\r\n\r\n if (Array.isArray(partial.$)) {\r\n let part: typeof partial = {}\r\n partial.$.forEach(p => merge(part, mergePartial(p)))\r\n delete partial.$\r\n merge(part, partial) // native win\r\n\r\n return part\r\n } else {\r\n return partial\r\n }\r\n}\r\n","export const VoidTags = [\r\n \"area\",\r\n \"base\",\r\n \"br\",\r\n \"col\",\r\n \"embed\",\r\n \"hr\",\r\n \"img\",\r\n \"input\",\r\n \"link\",\r\n \"meta\",\r\n \"source\",\r\n \"track\",\r\n \"wbr\",\r\n] as const;\r\n\r\nexport type VoidTagName = (typeof VoidTags)[number];\r\n\r\n","export const SvgTags = [\"svg\", \"circle\", \"path\", \"rect\", \"ellipse\",\r\n \"line\", \"polyline\", \"polygon\", \"g\", \"defs\",\r\n \"use\", \"symbol\", \"linearGradient\", \"radialGradient\",\r\n \"stop\", \"clipPath\", \"mask\", \"filter\", \"text\",\r\n \"tspan\", \"textPath\", \"image\", \"pattern\", \"marker\",\r\n \"animate\", \"animateTransform\", \"animateMotion\",\r\n \"feGaussianBlur\", \"feComposite\", \"feColorMatrix\",\r\n \"feMerge\", \"feMergeNode\", \"feOffset\", \"feFlood\",\r\n \"feBlend\", \"foreignObject\"]","export const BooleanAttributes = [\r\n \"allowFullScreen\",\r\n \"async\",\r\n \"autoFocus\",\r\n \"autoPlay\",\r\n \"checked\",\r\n \"compact\",\r\n \"contentEditable\",\r\n \"controls\",\r\n \"declare\",\r\n \"default\",\r\n \"defer\",\r\n \"disabled\",\r\n \"formNoValidate\",\r\n \"hidden\",\r\n \"isMap\",\r\n \"itemScope\",\r\n \"loop\",\r\n \"multiple\",\r\n \"muted\",\r\n \"noHref\",\r\n \"noShade\",\r\n \"noValidate\",\r\n \"open\",\r\n \"playsInline\",\r\n \"readonly\",\r\n \"required\",\r\n \"reversed\",\r\n \"scoped\",\r\n \"selected\",\r\n \"sortable\",\r\n \"trueSpeed\",\r\n \"typeMustMatch\",\r\n \"wmode\",\r\n \"autoCapitalize\",\r\n \"translate\",\r\n \"spellCheck\",\r\n \"inert\",\r\n \"download\",\r\n \"noModule\",\r\n \"paused\",\r\n \"autoPictureInPicture\",\r\n] as const;","//browserslist query \"> 0.1%, not dead\"\r\nexport const PrefixCSS: Record<string, string[]> = {\r\n transform: [\"webkit\", \"ms\"],\r\n transition: [\"webkit\", \"ms\"],\r\n animation: [\"webkit\"],\r\n userSelect: [\"webkit\", \"ms\"],\r\n flexDirection: [\"webkit\", \"ms\"],\r\n flexWrap: [\"webkit\", \"ms\"],\r\n justifyContent: [\"webkit\", \"ms\"],\r\n alignItems: [\"webkit\", \"ms\"],\r\n alignSelf: [\"webkit\", \"ms\"],\r\n order: [\"webkit\", \"ms\"],\r\n flexGrow: [\"webkit\", \"ms\"],\r\n flexShrink: [\"webkit\", \"ms\"],\r\n flexBasis: [\"webkit\", \"ms\"],\r\n columns: [\"webkit\"],\r\n columnCount: [\"webkit\"],\r\n columnGap: [\"webkit\"],\r\n columnRule: [\"webkit\"],\r\n columnWidth: [\"webkit\"],\r\n boxSizing: [\"webkit\"],\r\n appearance: [\"webkit\", \"moz\"],\r\n filter: [\"webkit\"],\r\n backdropFilter: [\"webkit\"],\r\n clipPath: [\"webkit\"],\r\n mask: [\"webkit\"],\r\n maskImage: [\"webkit\"],\r\n textSizeAdjust: [\"webkit\", \"ms\"],\r\n hyphens: [\"webkit\", \"ms\"],\r\n writingMode: [\"webkit\", \"ms\"],\r\n gridTemplateColumns: [\"ms\"],\r\n gridTemplateRows: [\"ms\"],\r\n gridAutoColumns: [\"ms\"],\r\n gridAutoRows: [\"ms\"],\r\n gridColumn: [\"ms\"],\r\n gridRow: [\"ms\"],\r\n marginInlineStart: [\"webkit\"],\r\n marginInlineEnd: [\"webkit\"],\r\n paddingInlineStart: [\"webkit\"],\r\n paddingInlineEnd: [\"webkit\"],\r\n minInlineSize: [\"webkit\"],\r\n maxInlineSize: [\"webkit\"],\r\n minBlockSize: [\"webkit\"],\r\n maxBlockSize: [\"webkit\"],\r\n inlineSize: [\"webkit\"],\r\n blockSize: [\"webkit\"],\r\n tabSize: [\"moz\"],\r\n overscrollBehavior: [\"webkit\", \"ms\"],\r\n touchAction: [\"ms\"],\r\n resize: [\"webkit\"],\r\n printColorAdjust: [\"webkit\"],\r\n backgroundClip: [\"webkit\"],\r\n boxDecorationBreak: [\"webkit\"],\r\n overflowScrolling: [\"webkit\"],\r\n};\r\n","export const CamelAttributes: string[] = [\r\n \"viewBox\",\r\n \"preserveAspectRatio\",\r\n \"gradientTransform\",\r\n \"gradientUnits\",\r\n \"spreadMethod\",\r\n \"markerStart\",\r\n \"markerMid\",\r\n \"markerEnd\",\r\n \"markerHeight\",\r\n \"markerWidth\",\r\n \"markerUnits\",\r\n \"refX\",\r\n \"refY\",\r\n \"patternContentUnits\",\r\n \"patternTransform\",\r\n \"patternUnits\",\r\n \"filterUnits\",\r\n \"primitiveUnits\",\r\n \"kernelUnitLength\",\r\n \"clipPathUnits\",\r\n \"maskContentUnits\",\r\n \"maskUnits\"\r\n] as const\r\n","import { escapeHTML, camelToKebab } from \"../helpers.js\";\nimport { BooleanAttributes, CamelAttributes } from \"../constants.js\";\nimport type { ElementNode } from \"./ElementNode.js\"\nimport { type AttributeValue } from \"../types.js\"\nimport { Notifier } from \"./Notifier.js\"\n\nexport class ElementAttribute {\n readonly name: string;\n readonly isBoolean: boolean;\n value: any;\n parent: ElementNode;\n _notifier = new Notifier();\n // Release handles for the reactive listener's state subscriptions, so a\n // re-set (e.g. patch() replacing a reactive value) can drop the old listener\n // instead of leaking it on the long-lived State until node removal.\n private _releases: (() => void)[] = [];\n\n constructor(name: string, value: any, parent: any) {\n this.parent = parent;\n this.isBoolean = (BooleanAttributes as readonly string[]).includes(name);\n if (CamelAttributes.includes(name)) {\n this.name = name\n } else {\n this.name = camelToKebab(name)\n }\n this.value = undefined;\n this.set(value);\n }\n\n render(): void {\n if (!this.parent || !this.parent.domElement) return;\n const domElement = this.parent.domElement;\n\n const mutateAttrs = [\"value\"];\n if (this.isBoolean) {\n if (this.value === false || this.value == null) {\n domElement.removeAttribute(this.name)\n } else {\n domElement.setAttribute(this.name, this.value === true ? \"\" : this.value)\n }\n } else if (this.value == null) {\n domElement.removeAttribute(this.name);\n } else if (mutateAttrs.includes(this.name)) {\n (domElement as any)[this.name] = this.value;\n } else {\n domElement.setAttribute(this.name, this.value);\n }\n }\n\n set(value: AttributeValue): void {\n let prev = this.value;\n\n // Drop any previous reactive subscription before (re)binding.\n if (this._releases.length) {\n for (const release of this._releases) release();\n this._releases = [];\n }\n\n if (value == null) {\n this.value = null;\n } else if (typeof value == \"function\") {\n let listener: any = () => {\n if (!this.parent || this.parent._disposed) return;\n let p = this.value;\n // Re-pass `listener` so states read only on a later run (conditional\n // dependencies) get subscribed too — matching children/style paths.\n this.value = this.isBoolean ? Boolean((value as Function)(listener)) : (value as Function)(listener);\n this.render();\n if (p !== this.value) this._notifier.notify(this.name, this.value);\n };\n\n listener.elementNode = this.parent!;\n listener.debug = `class:${this.parent?.tagName}_${this.parent?.nodeId} attribute:${this.name}`;\n\n listener.onSubscribe = (release: () => void) => {\n this._releases.push(release);\n if (this.parent) {\n this.parent.addHook(\"BeforeRemove\", () => {\n release();\n listener = null;\n });\n }\n };\n\n this.value = this.isBoolean ? Boolean(value(listener)) : value(listener);\n } else {\n this.value = this.isBoolean ? Boolean(value) : value;\n }\n\n this.render();\n if (prev !== this.value) this._notifier.notify(this.name, this.value);\n }\n\n addListener(callback: (value: any) => void): void {\n const handler = callback as any\n handler.onSubscribe = (release: () => void) => this.parent?.addHook(\"BeforeRemove\", release);\n this._notifier.addListener(this.name, handler)\n }\n\n remove(): void {\n if (this.parent && this.parent.attributes) {\n this.parent.attributes.remove(this.name);\n }\n this._dispose();\n }\n\n _dispose(): void {\n this._notifier._dispose();\n this.value = null;\n this.parent = null as any\n }\n\n generateHTML(): string {\n const { name, value } = this;\n if (this.isBoolean) {\n return value ? `${name}` : \"\";\n } else {\n const val = Array.isArray(value) ? JSON.stringify(value) : value;\n return `${name}=\"${escapeHTML(String(val))}\"`;\n }\n }\n}\n","import type { ElementNode } from \"./ElementNode.js\";\nimport { ElementAttribute } from \"./ElementAttribute.js\";\nimport { BooleanAttributes } from \"../constants.js\";\nimport { AttributeValue } from \"../types.js\"\n\nexport class AttributeList {\n items: Record<string, ElementAttribute> | null = {};\n parent: ElementNode | null;\n\n constructor(parent: ElementNode) {\n this.parent = parent;\n }\n\n generateHTML(): string {\n if (!this.items) return \"\";\n const str = Object.values(this.items)\n .map((attr) => attr.generateHTML())\n .join(\" \");\n return str ? ` ${str}` : \"\";\n }\n\n get(name: string): any {\n if (!this.items) return undefined;\n return this.items[name]?.value;\n }\n\n set(name: string, value: AttributeValue): void {\n if (!this.items || !this.parent) return;\n if (this.items[name]) {\n this.items[name].set(value);\n } else {\n this.items[name] = new ElementAttribute(name, value, this.parent);\n }\n }\n\n addListener(name: string, callback: (value: string | number) => void): void {\n if (this.has(name)) {\n this.items![name].addListener(callback);\n }\n }\n\n has(name: string): boolean {\n if (!this.items) return false;\n return Object.prototype.hasOwnProperty.call(this.items, name);\n }\n\n remove(name: string): void {\n if (!this.items) return;\n\n if (this.items[name]) {\n this.items[name]._dispose();\n delete this.items[name];\n }\n\n if (this.parent && this.parent.domElement && this.parent.domElement instanceof Element) {\n this.parent.domElement.removeAttribute(name);\n }\n }\n\n _dispose(): void {\n if (this.items) {\n for (const key in this.items) {\n this.items[key]._dispose();\n }\n }\n this.items = null;\n this.parent = null;\n }\n\n toggle(name: string, force?: boolean): void {\n if (\n !BooleanAttributes.includes(name as (typeof BooleanAttributes)[number])\n ) {\n throw Error(`${name} is not a boolean attribute`);\n }\n if (force === true) {\n this.set(name, true);\n } else if (force === false) {\n this.remove(name);\n } else {\n this.has(name) ? this.remove(name) : this.set(name, true);\n }\n }\n\n addClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n\n const add = (classes: string, newClass: string) => {\n const list = (classes || \"\").split(\" \").filter((e: string) => e);\n !list.includes(newClass) && list.push(className)\n return list.join(\" \")\n }\n\n let current = this.get(\"class\");\n\n if (typeof current === \"function\") {\n this.set(\"class\", () => add(current(), className));\n } else {\n this.set(\"class\", add(current, className))\n }\n }\n\n hasClass(className: string): boolean {\n if (!className || typeof className !== \"string\") return false;\n const current = this.get(\"class\") || \"\";\n const list = current.split(\" \").filter((e: string) => e);\n return list.includes(className);\n }\n\n toggleClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n this.hasClass(className)\n ? this.removeClass(className)\n : this.addClass(className);\n }\n\n removeClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n const current = this.get(\"class\") || \"\";\n const list: string[] = current.split(\" \").filter((e: string) => e);\n const updated = list.filter((cls) => cls !== className);\n updated.length > 0\n ? this.set(\"class\", updated.join(\" \"))\n : this.remove(\"class\");\n }\n\n replaceClass(oldClass: string, newClass: string): void {\n if (\n !oldClass ||\n !newClass ||\n typeof oldClass !== \"string\" ||\n typeof newClass !== \"string\"\n )\n return;\n if (this.hasClass(oldClass)) {\n this.removeClass(oldClass);\n this.addClass(newClass);\n }\n }\n}\n","import { ElementNode } from \"./ElementNode.js\";\r\nimport { isHTML, escapeHTML } from \"../helpers.js\";\r\n\r\nexport class TextNode {\r\n type = \"TextNode\"\r\n parent: ElementNode;\r\n text: string;\r\n domText?: ChildNode;\r\n\r\n constructor(textContent: string | number, parent: ElementNode) {\r\n this.parent = parent;\r\n this.text = textContent === \"\" ? \"\\u200B\" : String(textContent);\r\n }\r\n _createDOMNode() {\r\n let newNode: ChildNode;\r\n if (isHTML(this.text)) {\r\n const tpl = document.createElement(\"template\");\r\n tpl.innerHTML = this.text.trim();\r\n newNode = tpl.content.firstChild || document.createTextNode(\"\");\r\n } else {\r\n newNode = document.createTextNode(this.text);\r\n }\r\n this.domText = newNode;\r\n return newNode;\r\n }\r\n\r\n _dispose(): void {\r\n this.domText = undefined;\r\n this.text = \"\";\r\n }\r\n\r\n generateHTML(): string {\r\n if (this.text === \"\\u200B\") return \"&#8203;\";\r\n // Mirror _createDOMNode: a single-root HTML string is intentional inline\r\n // HTML, anything else is plain text and must be escaped so the server\r\n // output is XSS-safe and parses back to the same text node the client\r\n // builds (otherwise hydration child alignment drifts).\r\n return isHTML(this.text) ? this.text : escapeHTML(this.text);\r\n }\r\n\r\n render(domText: ChildNode | DocumentFragment | HTMLElement): void {\r\n const newNode = this._createDOMNode();\r\n domText.appendChild(newNode);\r\n }\r\n}","import { TextNode } from \"./TextNode.js\";\r\nimport { ElementNode } from \"./ElementNode.js\";\r\nimport type { DomphyElement } from \"../types.js\";\r\nimport { ensureDomStyle, getTagName } from \"../helpers.js\";\r\n\r\ntype ElementInput = DomphyElement | null | undefined | number | string\r\ntype NodeItem = ElementNode | TextNode;\r\n\r\nexport class ElementList {\r\n items: NodeItem[] = [];\r\n owner: ElementNode;\r\n _nextKey: number = 0;\r\n\r\n constructor(parent: ElementNode) {\r\n this.owner = parent;\r\n }\r\n\r\n _createNode(element: ElementInput | DomphyElement): NodeItem {\r\n return (typeof element === \"object\" && element !== null)\r\n ? new ElementNode(element, this.owner, this._nextKey++)\r\n : new TextNode(element == null ? \"\" : String(element), this.owner);\r\n }\r\n\r\n _moveDomElement(node: NodeItem, index: number) {\r\n if (!this.owner || !this.owner.domElement) return;\r\n const dom = this.owner.domElement;\r\n\r\n const el = node instanceof ElementNode ? node.domElement : node.domText;\r\n if (el) {\r\n const currentRef = dom.childNodes[index] || null;\r\n if (el !== currentRef) {\r\n dom.insertBefore(el, currentRef);\r\n }\r\n }\r\n }\r\n\r\n _swapDomElement(aNode: NodeItem, bNode: NodeItem) {\r\n if (!this.owner || !this.owner.domElement) return;\r\n const parent = this.owner.domElement;\r\n\r\n const a = aNode instanceof ElementNode ? aNode.domElement : aNode.domText;\r\n const b = bNode instanceof ElementNode ? bNode.domElement : bNode.domText;\r\n if (!a || !b) return;\r\n\r\n const aNext = a.nextSibling;\r\n const bNext = b.nextSibling;\r\n\r\n parent.insertBefore(a, bNext);\r\n parent.insertBefore(b, aNext);\r\n }\r\n\r\n update(inputs: ElementInput[], updateDom = true, silent = false): void {\r\n\r\n const oldItems = this.items.slice(); // snapshot for cleanup\r\n\r\n // keyed lookup from old list\r\n const keyed = new Map<string | number, NodeItem>();\r\n for (const item of oldItems) {\r\n if (item instanceof ElementNode && item.key !== null && item.key !== undefined) {\r\n keyed.set(item.key, item);\r\n }\r\n }\r\n\r\n if (!silent && this.owner.domElement) this.owner._hooks?.BeforeUpdate?.(this.owner, inputs);\r\n\r\n const oldSet = new Set<NodeItem>(oldItems);\r\n const claimed = new Set<NodeItem>();\r\n\r\n // build target order using existing ops (mutating this.items)\r\n for (let i = 0; i < inputs.length; i++) {\r\n const input = inputs[i];\r\n const isObj = typeof input === \"object\" && input !== null;\r\n const key = isObj ? (input as any)._key : undefined;\r\n const tag = isObj ? getTagName(input as DomphyElement) : undefined;\r\n\r\n // Keyed reuse: same key + same tag → reuse the node and patch it in place\r\n // (preserves DOM identity/state while reflecting new data).\r\n if (key !== undefined) {\r\n const reused = keyed.get(key);\r\n if (reused instanceof ElementNode && reused.tagName === tag) {\r\n keyed.delete(key);\r\n const cur = this.items.indexOf(reused);\r\n if (cur !== i && cur >= 0) {\r\n const isPortal = !!reused._portal;\r\n this.move(cur, i, isPortal ? false : updateDom, true);\r\n }\r\n reused.parent = this.owner as any;\r\n reused.patch(input as DomphyElement);\r\n claimed.add(reused);\r\n continue;\r\n }\r\n // key present but no tag-compatible match → fall through to insert; any\r\n // stale keyed node keeps its slot in `keyed` and is removed below.\r\n } else if (isObj) {\r\n // Unkeyed positional reuse: reuse the old unkeyed element already sitting\r\n // at this slot if its tag matches — this is what preserves focus, scroll,\r\n // selection, IME and uncontrolled input values across plain list updates.\r\n const at = this.items[i];\r\n if (at instanceof ElementNode && at.key == null && at.tagName === tag\r\n && oldSet.has(at) && !claimed.has(at)) {\r\n at.parent = this.owner as any;\r\n at.patch(input as DomphyElement);\r\n claimed.add(at);\r\n continue;\r\n }\r\n }\r\n\r\n claimed.add(this.insert(input, i, updateDom, true));\r\n }\r\n\r\n // Remove leftover nodes beyond the new length. Iterate a SNAPSHOT (not a\r\n // `while length > inputs.length` loop): a removal may defer (async exit\r\n // animation), leaving the node in `items`, so a length-based loop would spin.\r\n const extras = this.items.slice(inputs.length);\r\n for (const node of extras) this.remove(node, updateDom, true);\r\n keyed.forEach((node) => this.remove(node, updateDom, true));\r\n if (!silent) this.owner._hooks?.Update?.(this.owner);\r\n }\r\n\r\n insert(input: ElementInput, index?: number, updateDom = true, silent = false): NodeItem {\r\n\r\n let length = this.items.length;\r\n const finalIndex = (typeof index !== \"number\" || isNaN(index) || index < 0 || index > length)\r\n ? length\r\n : index;\r\n const item = this._createNode(input);\r\n this.items.splice(finalIndex, 0, item);\r\n\r\n if (item instanceof ElementNode) {\r\n //Parent always insert/mount before children\r\n item._hooks.Insert && item._hooks.Insert(item)\r\n\r\n let domElement = this.owner.domElement;\r\n if (updateDom && domElement) {\r\n\r\n\r\n if (item._portal) {\r\n let domElement = item._portal!(this.owner.getRoot())\r\n domElement && item.render(domElement)\r\n } else {\r\n let domNode = item._createDOMNode();\r\n const ref = domElement.childNodes[finalIndex] ?? null;\r\n domElement.insertBefore(domNode, ref);\r\n let root = domElement.getRootNode()\r\n const styleParent = root instanceof ShadowRoot ? root : document.head\r\n let domStyle = ensureDomStyle(styleParent)\r\n item.styles.render(domStyle as HTMLStyleElement)\r\n item._hooks.Mount && item._hooks.Mount(item)\r\n item.children.items.forEach(child => {\r\n if (child instanceof ElementNode && child._portal) {\r\n let dom = child._portal!(child.getRoot())\r\n dom && child.render(dom)\r\n } else {\r\n child.render(domNode)\r\n }\r\n })\r\n }\r\n }\r\n\r\n\r\n\r\n } else {\r\n let domElement = this.owner.domElement;\r\n if (updateDom && domElement) {\r\n let domNode = item._createDOMNode();\r\n const ref = domElement.childNodes[finalIndex] ?? null;\r\n domElement.insertBefore(domNode, ref);\r\n }\r\n }\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n return item;\r\n }\r\n\r\n remove(item: NodeItem, updateDom = true, silent = false): void {\r\n\r\n const index = this.items.indexOf(item);\r\n if (index < 0) return;\r\n\r\n if (item instanceof ElementNode) {\r\n // Guard against re-entrant removal of a node whose (deferred) removal is\r\n // already in flight — otherwise update()'s extras + keyed passes could\r\n // fire its BeforeRemove/animation twice. Synchronous removals are already\r\n // guarded by the indexOf check above (the node is spliced before re-entry).\r\n if (item._beforeRemoveFired) return;\r\n const done = () => {\r\n const el = item.domElement\r\n // Re-resolve position at completion time — a deferred (animated) removal\r\n // may run after other inserts/removes have shifted indices.\r\n const i = this.items.indexOf(item);\r\n if (i >= 0) this.items.splice(i, 1);\r\n updateDom && el && el.remove()\r\n item._dispose(); // _dispose fires Remove + releases subscriptions for the whole subtree\r\n }\r\n if (item._hooks.BeforeRemove && item.domElement) {\r\n let doneCalled = false;\r\n const onceDone = () => { if (!doneCalled) { doneCalled = true; done(); } };\r\n item._beforeRemoveFired = true; // prevent _dispose from re-firing BeforeRemove\r\n item._hooks.BeforeRemove(item, onceDone);\r\n // Auto-complete only for sync cleanup hooks. A hook that declares `done`\r\n // (arity >= 2, e.g. an exit animation) owns completion and defers removal.\r\n if ((item._hooks.BeforeRemove as Function).length < 2 && !doneCalled) onceDone();\r\n } else {\r\n done()\r\n }\r\n\r\n } else {\r\n const el = item.domText\r\n this.items.splice(index, 1);\r\n updateDom && el && el.remove()\r\n item._dispose();\r\n }\r\n\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n }\r\n\r\n clear(updateDom = true, silent = false): void {\r\n if (this.items.length === 0) return;\r\n const snapshot = this.items.slice();\r\n\r\n for (const item of snapshot) {\r\n this.remove(item, updateDom, true);\r\n }\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n }\r\n\r\n _dispose(): void {\r\n this.items.forEach(child => child._dispose())\r\n this.items = [];\r\n }\r\n\r\n swap(aIndex: number, bIndex: number, updateDom = true, silent = false) {\r\n if (aIndex < 0 || bIndex < 0 ||\r\n aIndex >= this.items.length || bIndex >= this.items.length ||\r\n aIndex === bIndex) return;\r\n\r\n const itemA = this.items[aIndex];\r\n const itemB = this.items[bIndex];\r\n\r\n this.items[aIndex] = itemB;\r\n this.items[bIndex] = itemA;\r\n\r\n if (updateDom) this._swapDomElement(itemA, itemB);\r\n\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n }\r\n\r\n move(fromIndex: number, toIndex: number, updateDom = true, silent = false): void {\r\n if (fromIndex < 0 || fromIndex >= this.items.length ||\r\n toIndex < 0 || toIndex >= this.items.length || fromIndex === toIndex) return;\r\n\r\n const item = this.items[fromIndex];\r\n\r\n this.items.splice(fromIndex, 1);\r\n this.items.splice(toIndex, 0, item);\r\n\r\n if (updateDom) this._moveDomElement(item, toIndex);\r\n\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n }\r\n\r\n generateHTML(): string {\r\n let html = \"\";\r\n for (const item of this.items) html += item.generateHTML();\r\n return html;\r\n }\r\n}\r\n","import type { StyleRule } from \"./StyleRule.js\";\r\nimport type { StyleValue } from \"../types.js\";\r\nimport { camelToKebab } from \"../helpers.js\";\r\nimport { PrefixCSS } from \"../constants.js\";\r\nimport { Listener } from \"../types.js\"\r\n\r\nexport class StyleProperty {\r\n name: string;\r\n cssName: string;\r\n value: StyleValue = \"\";\r\n parentRule: StyleRule;\r\n\r\n constructor(name: string, value: StyleValue, parentRule: StyleRule) {\r\n this.name = name;\r\n this.cssName = camelToKebab(name);\r\n this.parentRule = parentRule;\r\n this.set(value);\r\n }\r\n\r\n _domUpdate(): void {\r\n if (!this.parentRule) return;\r\n const domRule = this.parentRule.domRule;\r\n\r\n if (domRule && (domRule as CSSStyleRule).style) {\r\n let style: CSSStyleDeclaration = (domRule as CSSStyleRule).style;\r\n style.setProperty(this.cssName, String(this.value));\r\n\r\n if (PrefixCSS[this.name]) {\r\n PrefixCSS[this.name].forEach((prefix) => {\r\n style.setProperty(`-${prefix}-${this.cssName}`, String(this.value));\r\n });\r\n }\r\n }\r\n }\r\n _dispose(): void {\r\n this.value = \"\";\r\n this.parentRule = null as any;\r\n }\r\n\r\n set(value: StyleValue): void {\r\n\r\n if (typeof value === \"function\") {\r\n let listener = (() => {\r\n if (!this.parentRule || this.parentRule.parentNode?._disposed) return;\r\n this.value = value(listener);\r\n this._domUpdate();\r\n }) as unknown as Listener;\r\n\r\n listener.onSubscribe = (release: () => void) => {\r\n this.parentRule.parentNode?.addHook(\"BeforeRemove\", () => {\r\n release();\r\n listener = null!;\r\n });\r\n };\r\n\r\n listener.elementNode = this.parentRule!.root!;\r\n listener.debug = `class:${this.parentRule?.root?.tagName}_${this.parentRule?.root?.nodeId} style:${this.name}`;\r\n this.value = value(listener);\r\n } else {\r\n this.value = value;\r\n }\r\n\r\n this._domUpdate();\r\n }\r\n\r\n remove(): void {\r\n if (!this.parentRule) return;\r\n\r\n if (this.parentRule.domRule instanceof CSSStyleRule) {\r\n const domStyle = this.parentRule.domRule.style;\r\n domStyle.removeProperty(this.cssName);\r\n\r\n if (PrefixCSS[this.name]) {\r\n PrefixCSS[this.name].forEach((prefix) => {\r\n domStyle.removeProperty(`-${prefix}-${this.cssName}`);\r\n });\r\n }\r\n }\r\n delete this.parentRule.styleBlock![this.name];\r\n this._dispose();\r\n }\r\n\r\n cssText(): string {\r\n let str = `${this.cssName}: ${this.value}`;\r\n if (PrefixCSS[this.name]) {\r\n PrefixCSS[this.name].forEach((prefix) => {\r\n str += `; -${prefix}-${this.cssName}: ${this.value}`;\r\n });\r\n }\r\n return str;\r\n }\r\n}","import { ElementNode } from \"./ElementNode.js\";\r\nimport { StyleProperty } from \"./StyleProperty.js\";\r\nimport { StyleList } from \"./StyleList.js\";\r\n\r\nexport class StyleRule {\r\n selectorText: string;\r\n domRule: CSSRule | CSSMediaRule | CSSKeyframesRule | null = null;\r\n styleList: StyleList | null;\r\n styleBlock: Record<string, StyleProperty> | null = {};\r\n parent: StyleRule | ElementNode | null;\r\n\r\n constructor(selectorText: string, parent: StyleRule | ElementNode) {\r\n this.selectorText = selectorText;\r\n this.styleList = new StyleList(this);\r\n this.parent = parent;\r\n }\r\n\r\n _dispose(): void {\r\n\r\n if (this.styleBlock) {\r\n for (const prop of Object.values(this.styleBlock)) {\r\n prop._dispose();\r\n }\r\n }\r\n\r\n if (this.styleList) {\r\n this.styleList._dispose();\r\n }\r\n\r\n this.styleBlock = null;\r\n this.styleList = null;\r\n this.domRule = null;\r\n this.parent = null;\r\n }\r\n\r\n get root() {\r\n let node = this.parent;\r\n while (node instanceof StyleRule) {\r\n node = node.parent;\r\n }\r\n return node;\r\n }\r\n\r\n get parentNode(): ElementNode | null {\r\n let root: any = this.parent;\r\n while (root && root instanceof StyleRule) {\r\n root = root.parent;\r\n }\r\n return root as ElementNode;\r\n }\r\n \r\n insertStyle(name: string, val: any): void {\r\n if (!this.styleBlock) return;\r\n if (this.styleBlock[name]) {\r\n this.styleBlock[name].set(val);\r\n } else {\r\n this.styleBlock[name] = new StyleProperty(name, val, this);\r\n }\r\n }\r\n\r\n removeStyle(name: string): void {\r\n if (!this.styleBlock) return;\r\n if (this.styleBlock[name]) {\r\n this.styleBlock[name].remove();\r\n }\r\n }\r\n\r\n cssText(): string {\r\n if (!this.styleBlock || !this.styleList) return \"\";\r\n const styleStr = Object.values(this.styleBlock).map(decl => decl.cssText()).join(\";\");\r\n const nested = this.styleList.cssText();\r\n return `${this.selectorText} { ${styleStr} ${nested} } `;\r\n }\r\n\r\n mount(domRule: CSSRule | CSSKeyframesRule): void {\r\n if (!domRule || !this.styleList) return;\r\n this.domRule = domRule;\r\n if (\"cssRules\" in domRule) {\r\n this.styleList.mount(domRule.cssRules as CSSRuleList);\r\n }\r\n }\r\n\r\n remove(): void {\r\n\r\n if (this.domRule && this.domRule.parentStyleSheet) {\r\n const sheet = this.domRule.parentStyleSheet;\r\n const rules = sheet.cssRules;\r\n for (let i = 0; i < rules.length; i++) {\r\n if (rules[i] === this.domRule) {\r\n sheet.deleteRule(i);\r\n break;\r\n }\r\n }\r\n }\r\n this._dispose();\r\n }\r\n\r\n render(domSheet: CSSStyleSheet | CSSGroupingRule) {\r\n if (!this.styleBlock || !this.styleList) return;\r\n const styleStr = Object.values(this.styleBlock).map(decl => decl.cssText()).join(\";\");\r\n try {\r\n if (!this.selectorText.startsWith(\"@\")) {\r\n const css = `${this.selectorText} { ${styleStr} }`;\r\n const index = domSheet.insertRule(css, domSheet.cssRules.length);\r\n const domRule = domSheet.cssRules[index];\r\n if (domRule && \"selectorText\" in domRule) {\r\n this.mount(domRule);\r\n }\r\n } else if (/^@(media|supports|container|layer)\\b/.test(this.selectorText)) {\r\n const index = domSheet.insertRule(`${this.selectorText} {}`, domSheet.cssRules.length);\r\n const domRule = domSheet.cssRules[index];\r\n if (\"cssRules\" in domRule) {\r\n this.mount(domRule as CSSGroupingRule);\r\n this.styleList.render(domRule as CSSGroupingRule);\r\n }\r\n } else if (this.selectorText.startsWith(\"@keyframes\") || this.selectorText.startsWith(\"@font-face\")) {\r\n const css = this.cssText();\r\n const index = domSheet.insertRule(css, domSheet.cssRules.length);\r\n const domRule = domSheet.cssRules[index];\r\n this.mount(domRule);\r\n }\r\n } catch (err) {\r\n console.warn(\"Failed to insert rule:\", this.selectorText, err);\r\n }\r\n }\r\n}","import { ElementNode } from \"./ElementNode.js\";\r\nimport { selectorSplitter, normalizeSelectorKey } from \"../helpers.js\";\r\nimport { StyleRule } from \"./StyleRule.js\";\r\n\r\nexport class StyleList {\r\n parent: StyleRule | ElementNode | null;\r\n items: StyleRule[] = [];\r\n domStyle: HTMLStyleElement | null = null;\r\n\r\n constructor(parent: StyleRule | ElementNode) {\r\n this.parent = parent;\r\n }\r\n\r\n get parentNode(): ElementNode | null {\r\n let root: any = this.parent;\r\n while (root && root instanceof StyleRule) {\r\n root = root.parent;\r\n }\r\n return root as ElementNode;\r\n }\r\n\r\n addCSS(obj: Record<string, any>, parentSelector: string = \"\"): void {\r\n if (!this.items || !this.parent) return;\r\n const basic: Record<string, any> = {};\r\n\r\n function getSelector(selector: string, prev: string): string {\r\n return selector.startsWith(\"&\")\r\n ? `${prev}${selector.slice(1)}`\r\n : `${prev} ${selector}`;\r\n }\r\n\r\n for (const selector in obj) {\r\n const value = obj[selector];\r\n let splitKeys = selectorSplitter(selector);\r\n for (let key of splitKeys) {\r\n const currentSelector = getSelector(key, parentSelector);\r\n if (/^@(container|layer|supports|media)\\b/.test(key)) {\r\n if (typeof value === \"object\" && value != null) {\r\n const rule = new StyleRule(key, this.parent);\r\n rule.styleList!.addCSS(value, parentSelector);\r\n this.items.push(rule);\r\n }\r\n } else if (key.startsWith(\"@keyframes\")) {\r\n const rule = new StyleRule(key, this.parent);\r\n rule.styleList!.addCSS(value, \"\");\r\n this.items.push(rule);\r\n } else if (key.startsWith(\"@font-face\")) {\r\n const rule = new StyleRule(key, this.parent);\r\n for (const k in value) rule.insertStyle(k, value[k]);\r\n this.items.push(rule);\r\n } else if (typeof value === \"object\" && value != null) {\r\n const rule = new StyleRule(currentSelector, this.parent);\r\n this.items.push(rule);\r\n for (const [k, v] of Object.entries(value)) {\r\n if (typeof v === \"object\" && v != null) {\r\n let newSelector = getSelector(k, currentSelector);\r\n if (k.startsWith(\"&\")) {\r\n this.addCSS(v, newSelector);\r\n } else {\r\n const r = rule.styleList!.insertRule(newSelector);\r\n r.styleList!.addCSS(v, newSelector);\r\n }\r\n } else {\r\n rule.insertStyle(k, v);\r\n }\r\n }\r\n } else {\r\n basic[key] = value;\r\n }\r\n }\r\n }\r\n\r\n if (Object.keys(basic).length) {\r\n const rule = new StyleRule(parentSelector, this.parent);\r\n for (const key in basic) rule.insertStyle(key, basic[key]);\r\n this.items.push(rule);\r\n }\r\n }\r\n\r\n cssText(): string {\r\n if (!this.items) return \"\";\r\n return this.items.map((rule) => rule.cssText()).join(\"\");\r\n }\r\n\r\n insertRule(selector: string): StyleRule {\r\n if (!this.items || !this.parent) return null as any;\r\n let rule = this.items.find((rule) => rule.selectorText === selector);\r\n if (!rule) {\r\n rule = new StyleRule(selector, this.parent);\r\n this.items.push(rule);\r\n }\r\n return rule;\r\n }\r\n\r\n hydrate(domRuleMap: Map<string, CSSRule>): void {\r\n if (!this.items) return;\r\n for (const rule of this.items) {\r\n const domRule = domRuleMap.get(normalizeSelectorKey(rule.selectorText));\r\n if (domRule) rule.mount(domRule as CSSRule);\r\n }\r\n }\r\n\r\n mount(domRuleList: CSSRuleList): void {\r\n if (!this.items) return;\r\n if (!domRuleList) throw Error(\"Require domRuleList argument\");\r\n let wrongCount = 0;\r\n const fixOddEven = (css: string) => css.replace(\"(odd)\", \"(2n+1)\").replace(\"(even)\", \"(2n)\");\r\n\r\n this.items.forEach((rule, i) => {\r\n const index = i - wrongCount;\r\n const domRule = domRuleList[index];\r\n if (!domRule) return;\r\n if (rule.selectorText.startsWith(\"@\") && domRule instanceof CSSKeyframesRule) {\r\n rule.mount(domRule);\r\n } else if (\"keyText\" in domRule) {\r\n rule.mount(domRule);\r\n } else if (\"selectorText\" in domRule) {\r\n if (domRule.selectorText !== fixOddEven(rule.selectorText)) {\r\n wrongCount += 1;\r\n } else {\r\n rule.mount(domRule);\r\n }\r\n } else if (\"cssRules\" in domRule) {\r\n rule.mount(domRule as CSSMediaRule);\r\n }\r\n });\r\n }\r\n\r\n render(dom: HTMLStyleElement | CSSGroupingRule) {\r\n if (dom instanceof HTMLStyleElement) {\r\n this.domStyle = dom\r\n this.items.forEach((rule) => rule.render(dom.sheet!));\r\n } else if (dom instanceof CSSGroupingRule) {\r\n this.items.forEach((rule) => rule.render(dom));\r\n }\r\n }\r\n\r\n _dispose(): void {\r\n\r\n if (this.items) {\r\n for (let i = 0; i < this.items.length; i++) {\r\n this.items[i]._dispose();\r\n }\r\n }\r\n\r\n this.items = [];\r\n this.parent = null;\r\n this.domStyle = null;\r\n }\r\n}\r\n","import type { DomphyElement, EventName, HookMap, TagName, PartialElement } from \"../types.js\";\r\nimport { AttributeList } from \"./AttributeList.js\";\r\nimport { ElementList } from \"./ElementList.js\";\r\nimport { StyleList } from \"./StyleList.js\";\r\nimport { validate, mergePartial, getTagName, deepClone, ensureDomStyle, collectCSSRules } from \"../helpers.js\";\r\nimport { merge, hashString } from \"../utils.js\";\r\nimport { SvgTags, VoidTags } from \"../constants.js\"\r\n\r\nexport class ElementNode {\r\n _disposed = false\r\n _beforeRemoveFired = false\r\n type = \"ElementNode\"\r\n parent: ElementNode | null = null;\r\n _portal?: (root: ElementNode) => HTMLElement;\r\n tagName: TagName;\r\n children = new ElementList(this);\r\n styles = new StyleList(this);\r\n attributes = new AttributeList(this);\r\n domElement?: HTMLElement | null = null;\r\n _hooks: HookMap = {};\r\n _events?: { [K in EventName]?: (event: Event, node: ElementNode) => void } | null = null;\r\n _boundEvents = new Set<EventName>();\r\n _context?: Record<string, any> = {};\r\n _metadata?: Record<string, any> = {};\r\n key?: string | number | null = null;\r\n nodeId: string\r\n\r\n constructor(domphyElement: DomphyElement, _parent: ElementNode | null = null, index = 0) {\r\n domphyElement = deepClone(domphyElement)\r\n validate(domphyElement)\r\n domphyElement.style = domphyElement.style || {}\r\n this.parent = _parent;\r\n this.tagName = getTagName(domphyElement) as TagName;\r\n domphyElement = mergePartial(domphyElement) as DomphyElement\r\n\r\n this.key = (domphyElement as any)._key ?? null;\r\n this._context = domphyElement._context || {}\r\n this._metadata = domphyElement._metadata || {}\r\n\r\n let tempPath = `${this.parent?.nodeId}.${index}`\r\n const str = JSON.stringify(domphyElement.style || {}, (k, v) => typeof v === \"function\" ? tempPath : v,);\r\n this.nodeId = hashString(tempPath + str)\r\n\r\n this.attributes!.addClass(`${this.tagName}_${this.nodeId}`);\r\n if (domphyElement._onSchedule) domphyElement._onSchedule(this, domphyElement)\r\n\r\n this.merge(domphyElement)\r\n\r\n const children = (domphyElement as any)[this.tagName];\r\n\r\n if (children != null && children != undefined) {\r\n if (typeof children === \"function\") {\r\n\r\n let listener: any = () => {\r\n if (this._disposed) return\r\n let input = children(listener)\r\n this.children!.update(Array.isArray(input) ? input : [input])\r\n }\r\n\r\n listener!.elementNode = this;\r\n listener!.debug = `class:${this.tagName}_${this.nodeId} children`;\r\n listener!.onSubscribe = (release: () => void) => this.addHook(\"BeforeRemove\", () => {\r\n release()\r\n listener = null\r\n });\r\n listener && listener();\r\n } else {\r\n this.children!.update(Array.isArray(children) ? children : [children])\r\n }\r\n }\r\n this._hooks.Init && this._hooks.Init(this)\r\n }\r\n\r\n _createDOMNode() {\r\n const svgNamespace = \"http://www.w3.org/2000/svg\"\r\n let node = SvgTags.includes(this.tagName)\r\n ? document.createElementNS(svgNamespace, this.tagName)\r\n : document.createElement(this.tagName)\r\n\r\n this.domElement = node as HTMLElement\r\n\r\n if (this._events) {\r\n for (const key in this._events) this._bindEvent(key as EventName)\r\n }\r\n\r\n if (this.attributes) {\r\n Object.values(this.attributes.items!).forEach(attr => attr.render())\r\n }\r\n return node\r\n }\r\n\r\n // Bind a DOM listener that dispatches LIVE from this._events, so patch() can\r\n // swap the handler (e.g. a list item's onClick closure after its data changes)\r\n // without detaching/reattaching the DOM listener.\r\n _bindEvent(eventName: EventName): void {\r\n if (!this.domElement || this._boundEvents.has(eventName)) return\r\n this._boundEvents.add(eventName)\r\n let fn: any = (event: Event) => this._events?.[eventName]?.(event, this)\r\n this.domElement.addEventListener(eventName, fn)\r\n this.addHook(\"BeforeRemove\", (n) => {\r\n n.domElement?.removeEventListener(eventName, fn)\r\n fn = null\r\n })\r\n }\r\n\r\n _dispose(): void {\r\n if (this._disposed) return\r\n this._disposed = true\r\n\r\n // Fire BeforeRemove so reactive-listener releases (registered as BeforeRemove\r\n // hooks via onSubscribe) actually run for this node. Descendants are torn\r\n // down through this recursive _dispose — not through ElementList.remove — so\r\n // without this their subscriptions to long-lived State/RecordState leak.\r\n // Skip if the async-removal path in ElementList already fired it.\r\n if (!this._beforeRemoveFired) {\r\n this._beforeRemoveFired = true\r\n this._hooks.BeforeRemove?.(this, () => { })\r\n }\r\n\r\n if (this.children) {\r\n this.children._dispose();\r\n }\r\n\r\n if (this.styles) {\r\n this.styles.items!.forEach((rule) => rule.remove());\r\n this.styles._dispose();\r\n }\r\n\r\n if (this.attributes) {\r\n this.attributes._dispose();\r\n }\r\n\r\n // _onRemove fires for every node in the subtree, not just the directly-removed one.\r\n this._hooks.Remove?.(this)\r\n\r\n this.domElement = null;\r\n this._hooks = {};\r\n this._events = null;\r\n this._context = {};\r\n this._metadata = {};\r\n this.parent = null;\r\n }\r\n merge(part: PartialElement) {\r\n merge(this._context, part._context)\r\n merge(this._metadata, part._metadata)\r\n\r\n const keys = Object.keys(part)\r\n for (let i = 0; i < keys.length; i++) {\r\n const originalKey = keys[i];\r\n const value = (part as any)[originalKey];\r\n if ([\"$\", \"_onSchedule\", \"_key\", \"_context\", \"_metadata\", \"style\", this.tagName].includes(originalKey)) {\r\n continue\r\n } else if ([\"_onInit\", \"_onInsert\", \"_onMount\", \"_onBeforeUpdate\", \"_onUpdate\", \"_onBeforeRemove\", \"_onRemove\"].includes(originalKey)) {\r\n this.addHook(originalKey.substring(3) as keyof HookMap, value);\r\n } else if (originalKey.startsWith(\"on\")) {\r\n this.addEvent(originalKey.substring(2).toLowerCase() as EventName, value);\r\n } else if (originalKey == \"_portal\") {\r\n this._portal = value\r\n } else if (originalKey == \"class\" && typeof value === \"string\") {\r\n this.attributes!.addClass(value);\r\n } else {\r\n this.attributes!.set(originalKey, value);\r\n }\r\n }\r\n if (part.style) {\r\n this.styles.addCSS(part.style || {}, `.${`${this.tagName}_${this.nodeId}`}`);\r\n }\r\n\r\n }\r\n\r\n // Update this live node IN PLACE from a fresh element description, preserving\r\n // its DOM element (and thus focus/scroll/selection/uncontrolled value) and its\r\n // children's identity. Used by list reconciliation to reuse a node by key\r\n // (keyed) or position (unkeyed) while reflecting new data, instead of\r\n // destroying and recreating the DOM. Styles and lifecycle hooks are NOT\r\n // re-applied (reused items share structure; hooks already ran). Reactive\r\n // content (a function child) keeps its own listener and is left untouched.\r\n patch(rawElement: DomphyElement): void {\r\n let element: any = deepClone(rawElement)\r\n element.style = element.style || {}\r\n element = mergePartial(element)\r\n\r\n // Children / content — recurse so grandchildren are reused/patched too.\r\n const content = element[this.tagName]\r\n if (typeof content !== \"function\") {\r\n const next = content == null ? [] : Array.isArray(content) ? content : [content]\r\n this.children.update(next, !!this.domElement, true)\r\n }\r\n\r\n if (element._context) merge(this._context, element._context)\r\n if (element._metadata) merge(this._metadata, element._metadata)\r\n\r\n // Rebuild attributes and events. Events are replaced (live dispatch in\r\n // _bindEvent reads this._events, so swapping the map is enough); attributes\r\n // present before but absent now are removed; the auto scope class is kept.\r\n const autoClass = `${this.tagName}_${this.nodeId}`\r\n const reserved = [\"$\", \"_onSchedule\", \"_key\", \"_context\", \"_metadata\", \"style\", this.tagName]\r\n const hookKeys = [\"_onInit\", \"_onInsert\", \"_onMount\", \"_onBeforeUpdate\", \"_onUpdate\", \"_onBeforeRemove\", \"_onRemove\"]\r\n const keep = new Set<string>([\"class\"])\r\n let userClass: string | null = null\r\n\r\n this._events = {}\r\n for (const key of Object.keys(element)) {\r\n if (reserved.includes(key) || hookKeys.includes(key) || key === \"_portal\") continue\r\n const value = element[key]\r\n if (key.startsWith(\"on\") && typeof value === \"function\") {\r\n this.addEvent(key.substring(2).toLowerCase() as EventName, value)\r\n } else if (key === \"class\" && typeof value === \"string\") {\r\n userClass = value\r\n } else {\r\n this.attributes!.set(key, value)\r\n keep.add(key)\r\n }\r\n }\r\n\r\n this.attributes!.set(\"class\", userClass ? `${autoClass} ${userClass}` : autoClass)\r\n\r\n if (this.attributes!.items) {\r\n for (const name of Object.keys(this.attributes!.items)) {\r\n if (!keep.has(name)) this.attributes!.remove(name)\r\n }\r\n }\r\n\r\n if (this._events) {\r\n for (const key in this._events) this._bindEvent(key as EventName)\r\n }\r\n }\r\n\r\n addEvent(name: EventName, callback: (event: Event, node: ElementNode) => void): void {\r\n\r\n this._events = this._events || {}\r\n\r\n let current = this._events[name]\r\n if (typeof current == \"function\") {\r\n this._events[name] = (event: Event, node: ElementNode) => {\r\n current!(event, node)\r\n callback(event, node)\r\n }\r\n } else {\r\n this._events[name] = callback\r\n }\r\n }\r\n\r\n addHook<K extends keyof HookMap>(name: K, callback: HookMap[K]): void {\r\n const current = this._hooks[name];\r\n\r\n if (typeof current === \"function\") {\r\n const composed = ((...args: any[]) => {\r\n (current as Function)(...args);\r\n (callback as Function)(...args);\r\n }) as HookMap[K];\r\n // Preserve the maximum declared arity across composed hooks. Removal logic\r\n // inspects BeforeRemove.length (>= 2 means the hook owns `done()`, e.g. an\r\n // exit animation); a naive (...args) wrapper would report 0 and break that.\r\n try {\r\n Object.defineProperty(composed, \"length\", {\r\n value: Math.max((current as Function).length, (callback as Function).length),\r\n configurable: true,\r\n });\r\n } catch { /* length non-configurable on some engines — best effort */ }\r\n this._hooks[name] = composed;\r\n } else {\r\n this._hooks[name] = callback;\r\n }\r\n }\r\n getRoot(): ElementNode {\r\n let root: ElementNode = this;\r\n while (root && root instanceof ElementNode && root.parent) {\r\n root = root.parent;\r\n }\r\n return root\r\n }\r\n\r\n getContext(name: string): any {\r\n let node: ElementNode | null = this;\r\n while (node && (!node._context || !Object.prototype.hasOwnProperty.call(node._context, name))) {\r\n node = node.parent;\r\n }\r\n return node && node._context ? node._context[name] : undefined;\r\n }\r\n\r\n setContext(name: string, value: any) {\r\n this._context = this._context || {}\r\n this._context[name] = value;\r\n }\r\n\r\n getMetadata(name: string): any {\r\n return this._metadata ? this._metadata[name] : undefined;\r\n }\r\n\r\n setMetadata(key: string, value: any) {\r\n this._metadata = this._metadata || {}\r\n this._metadata[key] = value;\r\n\r\n }\r\n\r\n generateCSS(): string {\r\n if (!this.styles || !this.children) return \"\";\r\n let css = this.styles.cssText()\r\n css += this.children.items.map(child => child instanceof ElementNode ? child.generateCSS() : \"\").join(\"\")\r\n return css\r\n }\r\n\r\n generateHTML(): string {\r\n if (!this.children || !this.attributes) return \"\";\r\n const attributes = this.attributes.generateHTML();\r\n // Void elements must not emit a closing tag — `<br></br>` is parsed by the\r\n // HTML tokenizer as two <br>, which corrupts hydration child alignment.\r\n if ((VoidTags as readonly string[]).includes(this.tagName)) {\r\n return `<${this.tagName}${attributes}>`;\r\n }\r\n const content = this.children.generateHTML();\r\n return `<${this.tagName}${attributes}>${content}</${this.tagName}>`;\r\n }\r\n\r\n mount(domElement: HTMLElement, domStyle?: HTMLStyleElement): void {\r\n if (!domElement) throw new Error(\"Missing dom node on bind\");\r\n this.domElement = domElement;\r\n\r\n if (this._events) {\r\n for (const key in this._events) this._bindEvent(key as EventName)\r\n }\r\n\r\n if (this.children) {\r\n this.children.items.forEach((child, i) => {\r\n const childNode = domElement.childNodes[i];\r\n if (!childNode) return;\r\n if (child instanceof ElementNode) {\r\n child.mount(childNode as HTMLElement);\r\n } else {\r\n // Bind the server-rendered text/inline-HTML node so that reactive\r\n // child updates after hydration can locate and replace it.\r\n child.domText = childNode;\r\n }\r\n });\r\n }\r\n\r\n // Attach reactive style declarations to the server-rendered stylesheet so\r\n // post-hydration updates mutate the existing CSSOM rules instead of being\r\n // silently dropped (StyleProperty._domUpdate needs a bound domRule). Done\r\n // once from the call that received the style element, walking the whole\r\n // subtree because per-node selectors are globally unique.\r\n if (domStyle) {\r\n const sheet = domStyle.sheet;\r\n if (sheet) this._hydrateStyles(collectCSSRules(sheet.cssRules, new Map()));\r\n }\r\n\r\n this._hooks.Mount && this._hooks.Mount(this)\r\n }\r\n\r\n _hydrateStyles(domRuleMap: Map<string, CSSRule>): void {\r\n this.styles?.hydrate(domRuleMap);\r\n if (this.children) {\r\n for (const child of this.children.items) {\r\n if (child instanceof ElementNode) child._hydrateStyles(domRuleMap);\r\n }\r\n }\r\n }\r\n\r\n render(domElement: HTMLElement | SVGElement | DocumentFragment): HTMLElement | SVGElement {\r\n const newNode = this._createDOMNode();\r\n domElement.appendChild(newNode)\r\n this._hooks.Mount && this._hooks.Mount(this)\r\n let domStyle = this.getRoot().styles.domStyle\r\n let root = domElement.getRootNode()\r\n const styleParent = root instanceof ShadowRoot ? root : document.head\r\n domStyle ||= ensureDomStyle(styleParent)\r\n this.styles.render(domStyle as HTMLStyleElement)\r\n this.children.items.forEach(child => {\r\n if (child instanceof ElementNode && child._portal) {\r\n let dom = child._portal!(this.getRoot())\r\n dom && child.render(dom)\r\n } else {\r\n child.render(newNode)\r\n }\r\n })\r\n return newNode;\r\n }\r\n\r\n remove() {\r\n if (this.parent) {\r\n this.parent.children.remove(this)\r\n } else {\r\n // Root removal must also run BeforeRemove/Remove (and release reactive\r\n // subscriptions across the whole tree via _dispose), honoring async done().\r\n const done = () => {\r\n this.domElement?.remove()\r\n this._dispose()\r\n }\r\n if (this._hooks.BeforeRemove && this.domElement) {\r\n let called = false\r\n const once = () => { if (!called) { called = true; done() } }\r\n this._beforeRemoveFired = true\r\n this._hooks.BeforeRemove(this, once)\r\n if ((this._hooks.BeforeRemove as Function).length < 2 && !called) once()\r\n } else {\r\n done()\r\n }\r\n }\r\n }\r\n}\r\n","import { Notifier } from \"./Notifier.js\"\r\n\r\ntype Listener = (...args: any[]) => void\r\n\r\nexport class RecordState<T extends Record<string, any> = Record<string, any>> {\r\n private _notifier = new Notifier()\r\n private _record: T\r\n readonly initialRecord: T\r\n\r\n constructor(record: T) {\r\n this.initialRecord = { ...record }\r\n this._record = { ...record }\r\n }\r\n\r\n get<K extends keyof T>(key: K, l?: Listener): T[K] {\r\n if (l) this._notifier.addListener(key as string, l)\r\n return this._record[key]\r\n }\r\n\r\n set<K extends keyof T>(key: K, value: T[K]): void {\r\n this._record[key] = value\r\n this._notifier.notify(key as string)\r\n }\r\n\r\n addListener<K extends keyof T>(key: K, fn: Listener): () => void {\r\n return this._notifier.addListener(key as string, fn)\r\n }\r\n\r\n removeListener<K extends keyof T>(key: K, fn: Listener): void {\r\n this._notifier.removeListener(key as string, fn)\r\n }\r\n\r\n reset<K extends keyof T>(key: K): void {\r\n this.set(key, this.initialRecord[key])\r\n }\r\n\r\n _dispose(): void {\r\n this._notifier._dispose()\r\n }\r\n}\r\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /** Human path to the offending node, e.g. \"div > ul > li\". */\n path: string;\n message: string;\n /** How to fix it. */\n hint?: string;\n}\n\nexport interface DiagnoseOptions {\n /**\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\n * Set false if your reactive functions have side effects.\n */\n runReactive?: boolean;\n}\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n// Inline these and the theme stops owning type scale / rhythm — use patches.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\nexport function diagnose(\n root: unknown,\n options: DiagnoseOptions = {},\n): Diagnostic[] {\n const out: Diagnostic[] = [];\n walk(root, \"\", out, false, options.runReactive !== false);\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n runReactive: boolean,\n): void {\n if (typeof node === \"function\") {\n if (!runReactive) return;\n let result: unknown;\n try {\n result = (node as (listener: unknown) => unknown)(() => {});\n } catch {\n return; // reactive fn threw without a real runtime — skip\n }\n walk(result, path, out, true, runReactive);\n return;\n }\n\n if (Array.isArray(node)) {\n if (dynamic) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n if (\n elementItems.length > 1 &&\n elementItems.some((item) => item._key === undefined)\n ) {\n out.push({\n rule: \"missing-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\n });\n }\n }\n node.forEach((child, index) =>\n walk(child, `${path}[${index}]`, out, false, runReactive),\n );\n return;\n }\n\n if (!isPlainObject(node)) return;\n\n const element = node;\n const tag = findTag(element);\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n if (!tag) {\n const contentKeys = Object.keys(element).filter(\n (key) =>\n !RESERVED.has(key) &&\n !key.startsWith(\"_on\") &&\n !key.startsWith(\"on\") &&\n !key.startsWith(\"data\") &&\n !key.startsWith(\"aria\"),\n );\n if (contentKeys.length === 1) {\n out.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n path: here,\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\n });\n }\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n out.push({\n rule: \"void-content\",\n severity: \"error\",\n path: here,\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\n });\n }\n\n if (isPlainObject(element.style)) {\n const style = element.style;\n for (const prop in style) {\n if (TYPOGRAPHY_STYLE.has(prop) && typeof style[prop] !== \"function\") {\n out.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n path: here,\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\n });\n }\n }\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Formats diagnostics as a readable report (one line per issue). */\nexport function format(diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return \"✓ No issues found.\";\n const icon = (s: Severity) =>\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\n return diagnostics\n .map(\n (d) =>\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n"],"mappings":"0bAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,YAAAE,ICAA,IAAAC,EAAA,GAAAC,EAAAD,EAAA,cAAAE,EAAA,WAAAC,ICAO,IAAMC,EAAkB,CAC7B,UACA,aACA,gBACA,iBACA,SACA,WACA,YACA,mBACA,WACA,UACA,UACA,gBACA,gBACA,oBACA,SACA,cACA,QACA,aACA,SACA,YACA,cACA,cACA,aACA,cACA,SACA,mBACA,YACA,UACA,UACA,UACA,aACA,UACA,YACA,YACA,aACA,UACA,SACA,eACA,mBACA,cACA,cACA,eACA,eACA,cACA,aACA,cACA,YACA,UACA,UACA,SACA,YACA,aACA,eACA,UACA,WACA,WACA,cACA,4BACA,WACA,YACA,WACA,eACA,YACA,WACA,YACA,eACA,WACA,iBACA,YACA,UACA,eACA,cACA,aACA,gBACA,gBACA,gBACA,cACA,kBACA,iBACA,iBACA,gBACA,eACA,sBACA,uBACA,qBACA,sBACA,mBACA,kBACA,oBACA,mBACA,iBACA,uBACA,qBACA,oBACA,YACA,YACF,EAEaC,EAAeD,EAAgB,OAAO,CAACE,EAAKC,IAAO,CAC9D,IAAMC,EAAMD,EAAG,MAAM,CAAC,EAAE,YAAY,EACpC,OAAAD,EAAIE,CAAG,EAAID,EACJD,CACT,EAAG,CAAC,CAAiF,ECvGxEG,EAAW,CACtB,IACA,OACA,UACA,UACA,QACA,QACA,IACA,OACA,aACA,KACA,SACA,SACA,UACA,OACA,OACA,MACA,WACA,OACA,WACA,KACA,MACA,UACA,MACA,SACA,MACA,KACA,KACA,KACA,WACA,aACA,SACA,SACA,OACA,KACA,KACA,KACA,KACA,KACA,KACA,SACA,SACA,IACA,SACA,MACA,QACA,MACA,MACA,QACA,SACA,KACA,OACA,MACA,OACA,OACA,QACA,MACA,WACA,SACA,KACA,WACA,SACA,SACA,IACA,QACA,UACA,MACA,WACA,IACA,KACA,KACA,OACA,IACA,OACA,UACA,SACA,OACA,QACA,SACA,OACA,SACA,MACA,UACA,MACA,QACA,QACA,KACA,WACA,WACA,QACA,KACA,QACA,OACA,QACA,KACA,QACA,IACA,KACA,MACA,QACA,MACA,MACA,MACA,OACA,OACA,SACA,OACA,QACA,KACA,UACA,gBACA,mBACA,SACA,WACA,SACA,OACA,OACA,UACA,UACA,gBACA,sBACA,cACA,mBACA,oBACA,oBACA,iBACA,eACA,UACA,UACA,UACA,UACA,UACA,iBACA,UACA,UACA,cACA,eACA,WACA,eACA,qBACA,cACA,SACA,eACA,SACA,gBACA,IACA,QACA,OACA,iBACA,SACA,OACA,WACA,QACA,OACA,UACA,UACA,WACA,WACA,iBACA,OACA,MACA,aACA,OACA,MACA,SACA,SACA,SACA,OACA,WACA,QACA,MACA,MACF,EK5KO,IAAMC,EAAW,CACtB,OACA,OACA,KACA,MACA,QACA,KACA,MACA,QACA,OACA,OACA,SACA,QACA,KACF,ECdaC,EAAU,CAAC,MAAO,SAAU,OAAQ,OAAQ,UACrD,OAAQ,WAAY,UAAW,IAAK,OACpC,MAAO,SAAU,iBAAkB,iBACnC,OAAQ,WAAY,OAAQ,SAAU,OACtC,QAAS,WAAY,QAAS,UAAW,SACzC,UAAW,mBAAoB,gBAC/B,iBAAkB,cAAe,gBACjC,UAAW,cAAe,WAAY,UACtC,UAAW,eAAe,EagB9B,IAAMC,EAAO,IAAI,IAAY,CAAC,GAAGC,EAAU,GAAGC,CAAO,CAAC,EAChDC,EAAO,IAAI,IAAYC,CAAQ,EAC/BC,EAAW,IAAI,IAAI,CACvB,IACA,QACA,OACA,UACA,WACA,WACF,CAAC,EAEKC,EAAmB,IAAI,IAAI,CAC/B,WACA,aACA,aACA,eACF,CAAC,EAED,SAASC,EAAcC,EAAkD,CACvE,OAAO,OAAOA,GAAU,UAAYA,IAAU,MAAQ,CAAC,MAAM,QAAQA,CAAK,CAC5E,CAEA,SAASC,EAAQC,EAAsD,CACrE,QAAWC,KAAOD,EAChB,GAAIV,EAAK,IAAIW,CAAG,EAAG,OAAOA,CAG9B,CAGO,SAASC,EACdC,EACAC,EAA2B,CAAC,EACd,CACd,IAAMC,EAAoB,CAAC,EAC3B,OAAAC,EAAKH,EAAM,GAAIE,EAAK,GAAOD,EAAQ,cAAgB,EAAK,EACjDC,CACT,CAEA,SAASC,EACPC,EACAC,EACAH,EACAI,EACAC,EACM,CACN,GAAI,OAAOH,GAAS,WAAY,CAC9B,GAAI,CAACG,EAAa,OAClB,IAAIC,EACJ,GAAI,CACFA,EAAUJ,EAAwC,IAAM,CAAC,CAAC,CAC5D,OAAQK,EAAA,CACN,MACF,CACAN,EAAKK,EAAQH,EAAMH,EAAK,GAAMK,CAAW,EACzC,MACF,CAEA,GAAI,MAAM,QAAQH,CAAI,EAAG,CACvB,GAAIE,EAAS,CACX,IAAMI,EAAeN,EAAK,OACvBO,GAAUjB,EAAciB,CAAK,GAAKf,EAAQe,CAAK,CAClD,EAEED,EAAa,OAAS,GACtBA,EAAa,KAAME,GAASA,EAAK,OAAS,MAAS,GAEnDV,EAAI,KAAK,CACP,KAAM,cACN,SAAU,UACV,KAAMG,GAAQ,SACd,QACE,6GACF,KAAM,yEACR,CAAC,CAEL,CACAD,EAAK,QAAQ,CAACO,EAAOE,IACnBV,EAAKQ,EAAO,GAAGN,CAAI,IAAIQ,CAAK,IAAKX,EAAK,GAAOK,CAAW,CAC1D,EACA,MACF,CAEA,GAAI,CAACb,EAAcU,CAAI,EAAG,OAE1B,IAAMP,EAAUO,EACVU,EAAMlB,EAAQC,CAAO,EACrBkB,EAAOD,EAAOT,EAAO,GAAGA,CAAI,MAAMS,CAAG,GAAKA,EAAOT,GAAQ,SAE/D,GAAI,CAACS,EAAK,CACR,IAAME,EAAc,OAAO,KAAKnB,CAAO,EAAE,OACtCC,GACC,CAACN,EAAS,IAAIM,CAAG,GACjB,CAACA,EAAI,WAAW,KAAK,GACrB,CAACA,EAAI,WAAW,IAAI,GACpB,CAACA,EAAI,WAAW,MAAM,GACtB,CAACA,EAAI,WAAW,MAAM,CAC1B,EACIkB,EAAY,SAAW,GACzBd,EAAI,KAAK,CACP,KAAM,cACN,SAAU,UACV,KAAMa,EACN,QAAS,IAAIC,EAAY,CAAC,CAAC,sDAC3B,KAAM,yEACR,CAAC,EAEH,MACF,CAEA,IAAMC,EAAUpB,EAAQiB,CAAG,EAY3B,GAVIxB,EAAK,IAAIwB,CAAG,GAAKG,IAAY,MAAQA,IAAY,QACnDf,EAAI,KAAK,CACP,KAAM,eACN,SAAU,QACV,KAAMa,EACN,QAAS,aAAaD,CAAG,iCAAiC,MAAM,QAAQG,CAAO,EAAI,QAAU,OAAOA,CAAO,KAC3G,KAAM,WAAWH,CAAG,sDACtB,CAAC,EAGCpB,EAAcG,EAAQ,KAAK,EAAG,CAChC,IAAMqB,EAAQrB,EAAQ,MACtB,QAAWsB,KAAQD,EACbzB,EAAiB,IAAI0B,CAAI,GAAK,OAAOD,EAAMC,CAAI,GAAM,YACvDjB,EAAI,KAAK,CACP,KAAM,oBACN,SAAU,UACV,KAAMa,EACN,QAAS,YAAYI,CAAI,4CACzB,KAAM,gHACR,CAAC,CAGP,CAEAhB,EAAKc,EAASF,EAAMb,EAAK,GAAOK,CAAW,CAC7C,CAGO,SAASa,EAAOC,EAAmC,CACxD,GAAIA,EAAY,SAAW,EAAG,MAAO,0BACrC,IAAMC,EAAQC,GACZA,IAAM,QAAU,SAAMA,IAAM,UAAY,SAAM,IAChD,OAAOF,EACJ,IACEG,GACC,GAAGF,EAAKE,EAAE,QAAQ,CAAC,KAAKA,EAAE,IAAI,KAAKA,EAAE,IAAI;AAAA,IAAOA,EAAE,OAAO,GAAGA,EAAE,KAAO;AAAA,WAASA,EAAE,IAAI,GAAK,EAAE,EAC/F,EACC,KAAK;AAAA,CAAI,CACd","names":["global_exports","__export","src_exports","src_exports","__export","diagnose","format","EventProperties","eventNameMap","acc","ev","key","HtmlTags","VoidTags","SvgTags","TAGS","I","te","VOID","ee","RESERVED","TYPOGRAPHY_STYLE","isPlainObject","value","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","result","e","elementItems","child","item","index","tag","here","contentKeys","content","style","prop","format","diagnostics","icon","s","d"]}
1
+ {"version":3,"sources":["../src/global.ts","../src/index.ts","../../core/src/types/EventProperties.ts","../../core/src/constants/HtmlTags.ts","../../core/src/classes/Notifier.ts","../../core/src/classes/Collector.ts","../../core/src/classes/State.ts","../../core/src/utils.ts","../../core/src/helpers.ts","../../core/src/constants/VoidTags.ts","../../core/src/constants/SvgTags.ts","../../core/src/constants/BooleanAttributes.ts","../../core/src/constants/PrefixCSS.ts","../../core/src/constants/CamelAttributes.ts","../../core/src/classes/ElementAttribute.ts","../../core/src/classes/AttributeList.ts","../../core/src/classes/TextNode.ts","../../core/src/classes/ElementList.ts","../../core/src/classes/StyleProperty.ts","../../core/src/classes/StyleRule.ts","../../core/src/classes/StyleList.ts","../../core/src/classes/ElementNode.ts","../../core/src/classes/RecordState.ts","../../core/src/classes/Reactive.ts","../src/diagnose.ts"],"sourcesContent":["export * as doctor from \"./index\";\n","// @domphy/doctor — static analyzer for Domphy element trees. Catches\r\n// non-idiomatic patterns (inline typography, void-tag content, missing/duplicate/\r\n// unstable _key on lists, unknown tags) so humans and AI agents get a feedback\r\n// loop to self-correct generated code. `validate()` is the aggregate entry point.\r\n\r\nexport type {\r\n DiagnoseOptions,\r\n Diagnostic,\r\n Severity,\r\n ValidationReport,\r\n ValidationSummary,\r\n} from \"./diagnose.js\";\r\nexport { diagnose, format, validate } from \"./diagnose.js\";\r\n","export const EventProperties = [\r\n \"onAbort\",\r\n \"onAuxClick\",\r\n \"onBeforeMatch\",\r\n \"onBeforeToggle\",\r\n \"onBlur\",\r\n \"onCancel\",\r\n \"onCanPlay\",\r\n \"onCanPlayThrough\",\r\n \"onChange\",\r\n \"onClick\",\r\n \"onClose\",\r\n \"onContextLost\",\r\n \"onContextMenu\",\r\n \"onContextRestored\",\r\n \"onCopy\",\r\n \"onCueChange\",\r\n \"onCut\",\r\n \"onDblClick\",\r\n \"onDrag\",\r\n \"onDragEnd\",\r\n \"onDragEnter\",\r\n \"onDragLeave\",\r\n \"onDragOver\",\r\n \"onDragStart\",\r\n \"onDrop\",\r\n \"onDurationChange\",\r\n \"onEmptied\",\r\n \"onEnded\",\r\n \"onError\",\r\n \"onFocus\",\r\n \"onFormData\",\r\n \"onInput\",\r\n \"onInvalid\",\r\n \"onKeyDown\",\r\n \"onKeyPress\",\r\n \"onKeyUp\",\r\n \"onLoad\",\r\n \"onLoadedData\",\r\n \"onLoadedMetadata\",\r\n \"onLoadStart\",\r\n \"onMouseDown\",\r\n \"onMouseEnter\",\r\n \"onMouseLeave\",\r\n \"onMouseMove\",\r\n \"onMouseOut\",\r\n \"onMouseOver\",\r\n \"onMouseUp\",\r\n \"onPaste\",\r\n \"onPause\",\r\n \"onPlay\",\r\n \"onPlaying\",\r\n \"onProgress\",\r\n \"onRateChange\",\r\n \"onReset\",\r\n \"onResize\",\r\n \"onScroll\",\r\n \"onScrollEnd\",\r\n \"onSecurityPolicyViolation\",\r\n \"onSeeked\",\r\n \"onSeeking\",\r\n \"onSelect\",\r\n \"onSlotChange\",\r\n \"onStalled\",\r\n \"onSubmit\",\r\n \"onSuspend\",\r\n \"onTimeUpdate\",\r\n \"onToggle\",\r\n \"onVolumeChange\",\r\n \"onWaiting\",\r\n \"onWheel\",\r\n \"onTouchStart\",\r\n \"onTouchMove\",\r\n \"onTouchEnd\",\r\n \"onTouchCancel\",\r\n \"onPointerDown\",\r\n \"onPointerMove\",\r\n \"onPointerUp\",\r\n \"onPointerCancel\",\r\n \"onPointerEnter\",\r\n \"onPointerLeave\",\r\n \"onPointerOver\",\r\n \"onPointerOut\",\r\n \"onGotPointerCapture\",\r\n \"onLostPointerCapture\",\r\n \"onCompositionStart\",\r\n \"onCompositionUpdate\",\r\n \"onCompositionEnd\",\r\n \"onTransitionEnd\",\r\n \"onTransitionStart\",\r\n \"onAnimationStart\",\r\n \"onAnimationEnd\",\r\n \"onAnimationIteration\",\r\n \"onFullscreenChange\",\r\n \"onFullscreenError\",\r\n \"onFocusIn\",\r\n \"onFocusOut\",\r\n] as const\r\n\r\nexport const eventNameMap = EventProperties.reduce((acc, ev) => {\r\n const key = ev.slice(2).toLowerCase() as keyof HTMLElementEventMap\r\n acc[key] = ev;\r\n return acc;\r\n}, {} as Partial<Record<keyof HTMLElementEventMap, (typeof EventProperties)[number]>>);\r\n","export const HtmlTags = [\r\n \"a\",\r\n \"abbr\",\r\n \"address\",\r\n \"article\",\r\n \"aside\",\r\n \"audio\",\r\n \"b\",\r\n \"base\",\r\n \"blockquote\",\r\n \"br\",\r\n \"button\",\r\n \"canvas\",\r\n \"caption\",\r\n \"cite\",\r\n \"code\",\r\n \"col\",\r\n \"colgroup\",\r\n \"data\",\r\n \"datalist\",\r\n \"dd\",\r\n \"del\",\r\n \"details\",\r\n \"dfn\",\r\n \"dialog\",\r\n \"div\",\r\n \"dl\",\r\n \"dt\",\r\n \"em\",\r\n \"fieldset\",\r\n \"figcaption\",\r\n \"figure\",\r\n \"footer\",\r\n \"form\",\r\n \"h1\",\r\n \"h2\",\r\n \"h3\",\r\n \"h4\",\r\n \"h5\",\r\n \"h6\",\r\n \"header\",\r\n \"hgroup\",\r\n \"i\",\r\n \"iframe\",\r\n \"img\",\r\n \"input\",\r\n \"ins\",\r\n \"kbd\",\r\n \"label\",\r\n \"legend\",\r\n \"li\",\r\n \"main\",\r\n \"map\",\r\n \"mark\",\r\n \"meta\",\r\n \"meter\",\r\n \"nav\",\r\n \"noscript\",\r\n \"object\",\r\n \"ol\",\r\n \"optgroup\",\r\n \"option\",\r\n \"output\",\r\n \"p\",\r\n \"param\",\r\n \"picture\",\r\n \"pre\",\r\n \"progress\",\r\n \"q\",\r\n \"rp\",\r\n \"rt\",\r\n \"ruby\",\r\n \"s\",\r\n \"samp\",\r\n \"section\",\r\n \"select\",\r\n \"slot\",\r\n \"small\",\r\n \"source\",\r\n \"span\",\r\n \"strong\",\r\n \"sub\",\r\n \"summary\",\r\n \"sup\",\r\n \"table\",\r\n \"tbody\",\r\n \"td\",\r\n \"template\",\r\n \"textarea\",\r\n \"tfoot\",\r\n \"th\",\r\n \"thead\",\r\n \"time\",\r\n \"title\",\r\n \"tr\",\r\n \"track\",\r\n \"u\",\r\n \"ul\",\r\n \"var\",\r\n \"video\",\r\n \"wbr\",\r\n \"bdi\",\r\n \"bdo\",\r\n \"math\",\r\n \"menu\",\r\n \"search\",\r\n \"area\",\r\n \"embed\",\r\n \"hr\",\r\n \"animate\",\r\n \"animateMotion\",\r\n \"animateTransform\",\r\n \"circle\",\r\n \"clipPath\",\r\n \"cursor\",\r\n \"defs\",\r\n \"desc\",\r\n \"ellipse\",\r\n \"feBlend\",\r\n \"feColorMatrix\",\r\n \"feComponentTransfer\",\r\n \"feComposite\",\r\n \"feConvolveMatrix\",\r\n \"feDiffuseLighting\",\r\n \"feDisplacementMap\",\r\n \"feDistantLight\",\r\n \"feDropShadow\",\r\n \"feFlood\",\r\n \"feFuncA\",\r\n \"feFuncB\",\r\n \"feFuncG\",\r\n \"feFuncR\",\r\n \"feGaussianBlur\",\r\n \"feImage\",\r\n \"feMerge\",\r\n \"feMergeNode\",\r\n \"feMorphology\",\r\n \"feOffset\",\r\n \"fePointLight\",\r\n \"feSpecularLighting\",\r\n \"feSpotLight\",\r\n \"feTile\",\r\n \"feTurbulence\",\r\n \"filter\",\r\n \"foreignObject\",\r\n \"g\",\r\n \"image\",\r\n \"line\",\r\n \"linearGradient\",\r\n \"marker\",\r\n \"mask\",\r\n \"metadata\",\r\n \"mpath\",\r\n \"path\",\r\n \"pattern\",\r\n \"polygon\",\r\n \"polyline\",\r\n \"prefetch\",\r\n \"radialGradient\",\r\n \"rect\",\r\n \"set\",\r\n \"solidColor\",\r\n \"stop\",\r\n \"svg\",\r\n \"switch\",\r\n \"symbol\",\r\n \"tbreak\",\r\n \"text\",\r\n \"textPath\",\r\n \"tspan\",\r\n \"use\",\r\n \"view\",\r\n];","import { Handler } from \"../types.js\"\r\n\r\ntype ChainEntry = [notifier: Notifier, event: string]\r\n\r\n// Shared across all instances to track the flush chain for circular detection.\r\nlet _chain: ChainEntry[] = []\r\n\r\n// Microtask scheduler. Older embedded Chromium runtimes (SketchUp 2020 /\r\n// 2021.0 ship CEF 64) predate `queueMicrotask` (added in Chrome 71). A\r\n// resolved Promise's `.then` runs as a microtask in the same checkpoint, so\r\n// it is the standard fallback. The `.catch` mimics `queueMicrotask`'s\r\n// behaviour of surfacing thrown errors to the global error handler rather\r\n// than silently becoming an unhandled-rejection.\r\nconst _microtask: (cb: () => void) => void =\r\n typeof queueMicrotask === \"function\"\r\n ? queueMicrotask\r\n : (cb) => {\r\n Promise.resolve().then(cb).catch((e) => {\r\n setTimeout(() => { throw e }, 0)\r\n })\r\n }\r\n\r\n// Cap on self-re-notifications within one settle burst. A converging update\r\n// (clamp/normalize) reaches a fixpoint in a pass or two; anything beyond this is\r\n// a genuinely diverging self-feedback loop and is stopped like a cycle.\r\nconst SELF_NOTIFY_CAP = 100\r\n\r\n// Batching: while `_batchDepth > 0`, every `notify` records its pending entry as\r\n// usual but does NOT schedule a flush. Notifiers that received writes during the\r\n// batch are collected in `_batchedNotifiers`; when the outermost batch ends they\r\n// are scheduled together, so the whole batch coalesces into a SINGLE microtask\r\n// flush instead of one per write. This composes with the existing per-event\r\n// `_pending` coalescing (repeated writes to the same event still collapse to one\r\n// entry) without ever double-flushing.\r\nlet _batchDepth = 0\r\nlet _batchedNotifiers: Set<Notifier> = new Set()\r\n\r\n// Run `fn`, deferring all flushes triggered inside it into one flush afterwards.\r\n// Nested batches collapse into the outermost one. Reentrant-safe: the stack is\r\n// restored and the batched set is flushed even if `fn` throws.\r\nexport function runBatched<T>(fn: () => T): T {\r\n _batchDepth++\r\n try {\r\n return fn()\r\n } finally {\r\n _batchDepth--\r\n if (_batchDepth === 0) {\r\n const notifiers = _batchedNotifiers\r\n _batchedNotifiers = new Set()\r\n for (const notifier of notifiers) notifier._scheduleFlush()\r\n }\r\n }\r\n}\r\n\r\nexport class Notifier {\r\n private _listeners: Record<string, Set<Handler>> | null = {}\r\n private _pending: Map<string, { args: unknown[], chain: ChainEntry[] }> = new Map()\r\n private _scheduled = false\r\n // Args currently being delivered per event (used to detect a self-update fixpoint).\r\n private _flushing: Map<string, unknown[]> = new Map()\r\n // Self-re-notification depth in the current settle burst (runaway guard).\r\n private _selfDepth = 0\r\n\r\n _dispose(): void {\r\n if (this._listeners) {\r\n for (const event in this._listeners) {\r\n this._listeners[event].clear()\r\n }\r\n }\r\n this._listeners = null\r\n }\r\n\r\n addListener(event: string, listener: Handler): () => void {\r\n if (!this._listeners) return () => {}\r\n\r\n if (typeof event !== \"string\" || typeof listener !== \"function\") {\r\n throw new Error(\"Event name must be a string, listener must be a function\")\r\n }\r\n\r\n if (!this._listeners[event]) {\r\n this._listeners[event] = new Set()\r\n }\r\n\r\n const release = () => this.removeListener(event, listener)\r\n\r\n if (this._listeners[event].has(listener)) return release\r\n\r\n this._listeners[event].add(listener)\r\n if (typeof listener.onSubscribe === \"function\") {\r\n listener.onSubscribe(release)\r\n }\r\n\r\n return release\r\n }\r\n\r\n removeListener(event: string, listener: Handler): void {\r\n if (!this._listeners) return\r\n\r\n const listeners = this._listeners[event]\r\n if (listeners && listeners.has(listener)) {\r\n listeners.delete(listener)\r\n if (listeners.size === 0) {\r\n delete this._listeners[event]\r\n }\r\n }\r\n }\r\n\r\n // Number of listeners subscribed to an event. Used by `computed` to stay lazy:\r\n // an unobserved computed only marks itself dirty on a dependency change and\r\n // defers recomputation until the next read.\r\n listenerCount(event: string): number {\r\n return this._listeners?.[event]?.size ?? 0\r\n }\r\n\r\n notify(event: string, ...args: unknown[]): void {\r\n if (!this._listeners) return\r\n if (!this._listeners[event]) return\r\n\r\n // A listener that re-sets its OWN state mid-flush shows up as [this,event] at\r\n // the TOP of the chain. That is a converging self-update (clamp/normalize),\r\n // not a cross-state cycle — let it re-propagate with a fresh chain. A deeper\r\n // match (intervening notifiers) is a real cycle and is still rejected.\r\n const top = _chain.length ? _chain[_chain.length - 1] : null\r\n const selfReentry = !!top && top[0] === this && top[1] === event\r\n\r\n if (selfReentry) {\r\n const inflight = this._flushing.get(event)\r\n // Same value as the one being delivered → fixpoint reached, stop quietly.\r\n if (inflight && inflight[0] === args[0]) return\r\n if (this._selfDepth >= SELF_NOTIFY_CAP) {\r\n console.error(`[Domphy] Runaway self-update on \"${event}\" — stopped after ${SELF_NOTIFY_CAP} iterations`)\r\n return\r\n }\r\n this._selfDepth++\r\n this._pending.set(event, { args, chain: [] })\r\n } else {\r\n if (this._isCircular(event)) return\r\n this._pending.set(event, { args, chain: [..._chain] })\r\n }\r\n\r\n // While batching, defer scheduling: just remember this notifier has pending\r\n // work so the outermost batch can flush it once. Outside a batch, schedule\r\n // the microtask flush immediately as before.\r\n if (_batchDepth > 0) {\r\n _batchedNotifiers.add(this)\r\n } else {\r\n this._scheduleFlush()\r\n }\r\n }\r\n\r\n // Schedule the microtask flush if one is not already pending. Idempotent, so a\r\n // batch flushing many notifiers (and concurrent direct notifies) never queues\r\n // two flushes for the same instance.\r\n _scheduleFlush(): void {\r\n if (this._scheduled) return\r\n this._scheduled = true\r\n _microtask(() => this._flushAll())\r\n }\r\n\r\n private _isCircular(event: string): boolean {\r\n const idx = _chain.findIndex(([n, e]) => n === this && e === event)\r\n if (idx === -1) return false\r\n\r\n const names = [..._chain.slice(idx).map(([, e]) => e), event]\r\n console.error(`[Domphy] Circular dependency detected:\\n ${names.join(\" → \")}`)\r\n return true\r\n }\r\n\r\n private _flushAll(): void {\r\n this._scheduled = false\r\n const pending = this._pending\r\n this._pending = new Map()\r\n\r\n for (const [event, { args, chain }] of pending) {\r\n _chain = chain\r\n this._flush(event, args)\r\n }\r\n _chain = []\r\n // Burst settled (no self-update re-queued anything) → reset the runaway guard.\r\n if (this._pending.size === 0) this._selfDepth = 0\r\n }\r\n\r\n private _flush(event: string, args: unknown[]): void {\r\n if (!this._listeners) return\r\n const listeners = this._listeners[event]\r\n if (!listeners) return\r\n\r\n _chain.push([this, event])\r\n this._flushing.set(event, args)\r\n\r\n for (const listener of [...listeners]) {\r\n if (!listeners.has(listener)) continue\r\n try {\r\n listener(...args)\r\n } catch (e) {\r\n console.error(e)\r\n }\r\n }\r\n\r\n this._flushing.delete(event)\r\n _chain.pop()\r\n }\r\n}\r\n","import type { Handler } from \"../types.js\"\r\n\r\n// A Collector is the bridge between auto-tracked reads (State.get / RecordState.get\r\n// called WITHOUT an explicit listener) and the existing Notifier subscription model.\r\n//\r\n// When a Collector is active and a reactive source is read, the source subscribes\r\n// the Collector's `handler` to its Notifier exactly as it would any other listener.\r\n// The Notifier hands back a `release` callback through `handler.onSubscribe`; the\r\n// Collector records every release it receives so the whole dependency set can be\r\n// torn down at once on the next re-run (effect/computed) or on dispose. This reuses\r\n// Notifier's subscribe/notify/flush and `_chain` cycle detection — there is no\r\n// parallel reactivity system.\r\nexport class Collector {\r\n // The function the Notifier actually stores as a listener. Invoked (via the\r\n // Notifier flush) whenever any tracked dependency changes.\r\n readonly handler: Handler\r\n // Release callbacks for the dependencies subscribed during the current run.\r\n private _releases: Set<() => void> = new Set()\r\n\r\n constructor(onDependencyChange: () => void) {\r\n const handler = (() => onDependencyChange()) as Handler\r\n // Notifier.addListener calls onSubscribe(release) right after adding the\r\n // listener. Record the release so we can drop this exact subscription later.\r\n handler.onSubscribe = (release: () => void) => {\r\n this._releases.add(release)\r\n }\r\n this.handler = handler\r\n }\r\n\r\n // Release every dependency subscribed since the last reset. Called before a\r\n // re-run (so stale deps are dropped and only freshly read deps remain) and on\r\n // dispose (so nothing is left subscribed).\r\n reset(): void {\r\n for (const release of this._releases) release()\r\n this._releases.clear()\r\n }\r\n\r\n get dependencyCount(): number {\r\n return this._releases.size\r\n }\r\n}\r\n\r\n// Stack of active collectors. A stack (not a single slot) so nested reactive\r\n// computations compose: a `computed` read inside an `effect` pushes its own\r\n// collector while running, then pops, restoring the effect as the active one.\r\nconst COLLECTOR_STACK: Collector[] = []\r\n\r\n// Depth of active `untrack` regions. While > 0, reads do NOT register into the\r\n// active collector even though one is on the stack.\r\nlet UNTRACK_DEPTH = 0\r\n\r\n// The collector that auto-tracked reads should subscribe to right now, or null\r\n// when tracking is suppressed (inside untrack) or no computation is running.\r\nexport function activeCollector(): Collector | null {\r\n if (UNTRACK_DEPTH > 0) return null\r\n return COLLECTOR_STACK.length ? COLLECTOR_STACK[COLLECTOR_STACK.length - 1] : null\r\n}\r\n\r\n// Run `fn` with `collector` active, guaranteeing the stack is restored even if\r\n// `fn` throws.\r\nexport function runWithCollector<T>(collector: Collector, fn: () => T): T {\r\n COLLECTOR_STACK.push(collector)\r\n try {\r\n return fn()\r\n } finally {\r\n COLLECTOR_STACK.pop()\r\n }\r\n}\r\n\r\n// Run `fn` with tracking suppressed; reads inside register nowhere.\r\nexport function runUntracked<T>(fn: () => T): T {\r\n UNTRACK_DEPTH++\r\n try {\r\n return fn()\r\n } finally {\r\n UNTRACK_DEPTH--\r\n }\r\n}\r\n","import { Notifier } from \"./Notifier.js\";\r\nimport { activeCollector } from \"./Collector.js\";\r\nimport { Handler } from \"../types.js\"\r\n\r\nexport type ValueListener<T> = ((_value: T) => void) & Handler\r\nexport type ValueOrState<T> = T | State<T>\r\n\r\nexport class State<T> {\r\n readonly _isState = true;\r\n private _value: T;\r\n readonly initialValue: T;\r\n private _notifier: Notifier | null = new Notifier();\r\n\r\n constructor(initialValue: T, readonly name: string = typeof initialValue) {\r\n this.initialValue = initialValue;\r\n this._value = initialValue;\r\n }\r\n\r\n get(listener?: ValueListener<T>): T {\r\n if (listener) {\r\n this.addListener(listener);\r\n } else {\r\n // Auto-tracking: with no explicit listener, subscribe the active collector\r\n // (a running computed/effect) so it re-runs when this state changes. When\r\n // no collector is active the read is a plain, untracked value read — the\r\n // original behavior is preserved exactly.\r\n const collector = activeCollector();\r\n if (collector) this.addListener(collector.handler as ValueListener<T>);\r\n }\r\n return this._value;\r\n }\r\n\r\n set(newValue: T): void {\r\n if (!this._notifier) return;\r\n this._value = newValue;\r\n this._notifier.notify(this.name, newValue);\r\n }\r\n\r\n reset(): void {\r\n this.set(this.initialValue);\r\n }\r\n\r\n addListener(listener: ValueListener<T>): () => void {\r\n if (!this._notifier) return () => { };\r\n return this._notifier.addListener(this.name, listener);\r\n }\r\n\r\n removeListener(listener: ValueListener<T>): void {\r\n if (!this._notifier) return;\r\n this._notifier.removeListener(this.name, listener);\r\n }\r\n\r\n _dispose(): void {\r\n if (this._notifier) {\r\n this._notifier._dispose();\r\n this._notifier = null;\r\n }\r\n }\r\n}\r\n","import { DomphyElement, HookMap, EventName, Handler, Listener } from \"./types.js\";\r\nimport { State } from \"./classes/State.js\"\r\n\r\nimport { deepClone, addEvent, addHook } from \"./helpers.js\"\r\n\r\nexport function merge(source: Record<string, any> = {}, target: Record<string, any> = {}): Record<string, any> {\r\n const comma = [\"animation\", \"transition\", \"boxShadow\", \"textShadow\", \"background\", \"fontFamily\"]\r\n const space = [\"class\", \"rel\", \"transform\", \"acceptCharset\", \"sandbox\"]\r\n const adjacent = [\"content\"]\r\n if (Object.prototype.toString.call(target) === \"[object Object]\" && Object.getPrototypeOf(target) === Object.prototype) { // plainjs not class instance\r\n target = deepClone(target)\r\n }\r\n\r\n for (const key in target) {\r\n\r\n const value = target[key];\r\n if (value === undefined || value === null || value === \"\") continue;\r\n\r\n if (typeof value === \"object\" && !Array.isArray(value)) {\r\n if (typeof source[key] === \"object\") {\r\n source[key] = merge(source[key], value);\r\n } else {\r\n source[key] = value;\r\n }\r\n\r\n } else {\r\n if (comma.includes(key)) {\r\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\r\n let old = source[key]\r\n source[key] = (listener: Handler) => {\r\n let val1 = typeof old === \"function\" ? old(listener) : old\r\n let val2 = typeof value === \"function\" ? value(listener) : value\r\n return [val1, val2].filter(e => e).join(\", \")\r\n }\r\n } else {\r\n source[key] = [source[key], value].filter(e => e).join(\", \")\r\n }\r\n\r\n } else if (adjacent.includes(key)) {\r\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\r\n let old = source[key]\r\n source[key] = (listener: Handler) => {\r\n let val1 = typeof old === \"function\" ? old(listener) : old\r\n let val2 = typeof value === \"function\" ? value(listener) : value\r\n return [val1, val2].filter(e => e).join(\"\")\r\n }\r\n } else {\r\n source[key] = [source[key], value].filter(e => e).join(\"\")\r\n }\r\n } else if (space.includes(key)) {\r\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\r\n let old = source[key]\r\n source[key] = (listener: Handler) => {\r\n let val1 = typeof old === \"function\" ? old(listener) : old\r\n let val2 = typeof value === \"function\" ? value(listener) : value\r\n return [val1, val2].filter(e => e).join(\" \")\r\n }\r\n } else {\r\n source[key] = [source[key], value].filter(e => e).join(\" \")\r\n }\r\n } else if (key.startsWith(\"on\")) {\r\n let name = key.replace(\"on\", \"\").toLowerCase() as EventName\r\n addEvent(source as DomphyElement, name, value)\r\n } else if (key.startsWith(\"_on\")) {\r\n let name = key.replace(\"_on\", \"\") as keyof HookMap\r\n addHook(source as DomphyElement, name, value)\r\n } else {\r\n source[key] = value;\r\n }\r\n }\r\n }\r\n return source;\r\n}\r\n\r\nexport function hashString(str: string = \"\"): string {\r\n let hash = 0x811c9dc5; // FNV-1a 32-bit offset basis\r\n for (let i = 0; i < str.length; i++) {\r\n hash ^= str.charCodeAt(i);\r\n hash = (hash * 0x01000193) >>> 0; // FNV prime, keep 32-bit unsigned\r\n }\r\n return String.fromCharCode(97 + (hash % 26)) + hash.toString(16);\r\n}\r\n\r\nexport function toState<T>(val: T | State<T>, name?: string): State<T> {\r\n return (val instanceof State || (val as any)?._isState) ? val as State<T> : new State<T>(val, name);\r\n}\r\n\r\nexport function r<T>(fn: (listener: Listener) => T): (listener: Listener) => T {\r\n return fn;\r\n}\r\n\r\n","import { DomphyElement, PartialElement, HookMap, EventName, Handler, TagName } from \"./types.js\";\r\nimport { ElementNode } from \"./classes/ElementNode.js\"\r\nimport { State } from \"./classes/State.js\"\r\nimport { eventNameMap } from \"./types/EventProperties.js\"\r\nimport { HtmlTags } from \"./constants/HtmlTags.js\"\r\nimport {merge} from \"./utils.js\"\r\n\r\nexport function addHook<K extends keyof HookMap>(partial: PartialElement, hookName: K, handler: HookMap[K]): void {\r\n const hookProperty = `_on${hookName}` as keyof PartialElement;\r\n let current = partial[hookProperty];\r\n\r\n if (typeof current === \"function\") {\r\n (partial as any)[hookProperty] = (...args: any[]) => {\r\n (current as Function)(...args);\r\n (handler as Function)(...args);\r\n };\r\n } else {\r\n (partial as any)[hookProperty] = handler;\r\n }\r\n}\r\n\r\nexport function addEvent<K extends keyof HTMLElementEventMap>(\r\n attributes: PartialElement,\r\n eventName: K,\r\n handler: (event: HTMLElementEventMap[K], node: ElementNode) => void\r\n): void {\r\n const eventProperty = eventNameMap[eventName];\r\n if (!eventProperty) {\r\n throw Error(`invalid event name \"${eventName}\"`);\r\n }\r\n const current = (attributes as any)[eventProperty]\r\n\r\n if (typeof current == \"function\") {\r\n (attributes as any)[eventProperty] = (event: HTMLElementEventMap[K], node: ElementNode) => {\r\n current(event, node)\r\n handler(event, node);\r\n };\r\n } else {\r\n (attributes as any)[eventProperty] = handler\r\n }\r\n}\r\n\r\nexport function deepClone(value: any, seen = new WeakMap()): any {\r\n if (value === null || typeof value !== \"object\") return value;\r\n if (typeof value === \"function\") return value;\r\n if (seen.has(value)) return seen.get(value);\r\n\r\n const proto = Object.getPrototypeOf(value);\r\n if (proto !== Object.prototype && !Array.isArray(value)) return value; // ignore class instance\r\n\r\n let clone: any;\r\n\r\n if (Array.isArray(value)) {\r\n clone = [];\r\n seen.set(value, clone);\r\n for (const v of value) clone.push(deepClone(v, seen));\r\n return clone;\r\n }\r\n\r\n if (value instanceof Date) return new Date(value);\r\n if (value instanceof RegExp) return new RegExp(value);\r\n if (value instanceof Map) {\r\n clone = new Map();\r\n seen.set(value, clone);\r\n for (const [k, v] of value) clone.set(deepClone(k, seen), deepClone(v, seen));\r\n return clone;\r\n }\r\n if (value instanceof Set) {\r\n clone = new Set();\r\n seen.set(value, clone);\r\n for (const v of value) clone.add(deepClone(v, seen));\r\n return clone;\r\n }\r\n if (ArrayBuffer.isView(value)) {\r\n return new (value as any).constructor(value);\r\n }\r\n if (value instanceof ArrayBuffer) {\r\n return value.slice(0);\r\n }\r\n\r\n clone = Object.create(proto);\r\n seen.set(value, clone);\r\n\r\n for (const key of Reflect.ownKeys(value)) {\r\n clone[key] = deepClone(value[key], seen);\r\n }\r\n\r\n return clone;\r\n}\r\n\r\nexport function validate(element: DomphyElement | PartialElement, asPartial = false): boolean {\r\n if (Object.prototype.toString.call(element) !== \"[object Object]\") {\r\n throw Error(`typeof ${element} is invalid DomphyElement`);\r\n }\r\n let keys = Object.keys(element);\r\n for (let i = 0; i < keys.length; i++) {\r\n let key = keys[i];\r\n let val = element[key as keyof typeof element]\r\n if (i == 0 && !HtmlTags.includes(key) && !key.includes(\"-\") && !asPartial) { // web-component\r\n throw Error(`key ${key} is not valid HTML tag name`);\r\n } else if (key == \"style\" && val && Object.prototype.toString.call(val) !== \"[object Object]\") {\r\n throw Error(`\"style\" must be a object`);\r\n } else if (key == \"$\") {\r\n element.$!.forEach(v => validate(v as PartialElement, true))\r\n } else if (key.startsWith(\"_on\") && typeof val != \"function\") {\r\n throw Error(`hook ${key} value \"${val}\" must be a function `)\r\n } else if (key.startsWith(\"on\") && typeof val != \"function\") {\r\n throw Error(`event ${key} value \"${val}\" must be a function `);\r\n } else if (key == \"_portal\" && typeof val !== \"function\") {\r\n throw Error(`\"_portal\" must be a function return HTMLElement`);\r\n } else if (key == \"_context\" && Object.prototype.toString.call(val) !== \"[object Object]\") {\r\n throw Error(`\"_context\" must be a object`);\r\n } else if (key == \"_metadata\" && Object.prototype.toString.call(val) !== \"[object Object]\") {\r\n throw Error(`\"_metadata\" must be a object`);\r\n } else if (key == \"_key\" && (typeof val !== \"string\" && typeof val !== \"number\")) {\r\n throw Error(`\"_key\" must be a string or number`);\r\n }\r\n }\r\n return true;\r\n}\r\n\r\nexport function isValid(element: DomphyElement): boolean {\r\n\r\n if (Array.isArray(element)) return false;\r\n if (!element || typeof element !== \"object\") return false;\r\n\r\n let keys = Object.keys(element);\r\n for (let i = 0; i < keys.length; i++) {\r\n let key = keys[i];\r\n let val = element[key as keyof typeof element];\r\n if (i == 0 && !HtmlTags.includes(key)) return false\r\n if (key === \"style\" && (val == null || typeof val !== \"object\" || Array.isArray(val))) return false;\r\n if (key.startsWith(\"_on\") && typeof val !== \"function\") return false;\r\n if (key.startsWith(\"on\") && typeof val !== \"function\") return false;\r\n if (key === \"_portalChildren\" && !Array.isArray(val)) return false;\r\n if ((key === \"_context\" || key === \"_metadata\") && (val == null || typeof val !== \"object\" || Array.isArray(val))) return false;\r\n }\r\n return true;\r\n}\r\n\r\nexport function isHTML(str: string): boolean {\r\n return /<([a-z][\\w-]*)(\\s[^>]*)?>.*<\\/\\1>|<([a-z][\\w-]*)(\\s[^>]*)?\\/>/i.test(str.trim());\r\n}\r\n\r\nexport function escapeHTML(str: string): string {\r\n return str\r\n .replace(/&/g, \"&amp;\")\r\n .replace(/</g, \"&lt;\")\r\n .replace(/>/g, \"&gt;\")\r\n .replace(/\"/g, \"&quot;\")\r\n .replace(/'/g, \"&#39;\");\r\n}\r\n\r\nexport function addClass(element: PartialElement, className: string): void {\r\n\r\n if (typeof element.class == \"function\") {\r\n let reactive = element.class\r\n element.class = (listener) => String(reactive(listener)) + \" \" + className\r\n } else {\r\n let current = element.class || \"\"\r\n let split = String(current).split(\" \")\r\n split.push(className)\r\n element.class = split.filter(e => e).join(\" \")\r\n }\r\n\r\n}\r\n\r\nexport function removeClass(element: PartialElement, className: string): void {\r\n\r\n if (typeof element.class == \"function\") {\r\n let reactive = element.class\r\n element.class = (listener) => {\r\n let split = String(reactive(listener)).split(\" \")\r\n return split.filter(e => e != className).join(\" \")\r\n }\r\n } else {\r\n let split = String(element.class).split(\" \")\r\n element.class ||= \"\"\r\n element.class = split.filter(e => e != className).join(\" \")\r\n }\r\n\r\n}\r\n\r\nexport function toggleClass(element: PartialElement, className: string): void {\r\n\r\n if (typeof element.class == \"function\") {\r\n let reactive = element.class\r\n element.class = (listener) => {\r\n let split = String(reactive(listener)).split(\" \")\r\n return split.includes(className) ? split.filter(e => e != className).join(\" \") : split.concat([className]).join(\" \")\r\n }\r\n } else {\r\n let split = String(element.class).split(\" \")\r\n element.class ||= \"\"\r\n element.class = split.includes(className) ? split.filter(e => e != className).join(\" \") : split.concat([className]).join(\" \")\r\n }\r\n\r\n}\r\n\r\nexport function getTagName(element: DomphyElement): TagName | undefined {\r\n return Object.keys(element).find(e => HtmlTags.includes(e)) as TagName | undefined\r\n}\r\n\r\n\r\nexport function camelToKebab(str: string): string {\r\n return str.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase();\r\n}\r\n\r\nexport function selectorSplitter(selectors: string) {\r\n if (selectors.indexOf('@') === 0) {\r\n return [selectors];\r\n }\r\n var splitted = [];\r\n var parens = 0;\r\n var angulars = 0;\r\n var soFar = '';\r\n for (var i = 0, len = selectors.length; i < len; i++) {\r\n var char = selectors[i];\r\n if (char === '(') {\r\n parens += 1;\r\n } else if (char === ')') {\r\n parens -= 1;\r\n } else if (char === '[') {\r\n angulars += 1;\r\n } else if (char === ']') {\r\n angulars -= 1;\r\n } else if (char === ',') {\r\n if (!parens && !angulars) {\r\n splitted.push(soFar.trim());\r\n soFar = '';\r\n continue;\r\n }\r\n }\r\n soFar += char;\r\n }\r\n splitted.push(soFar.trim());\r\n return splitted;\r\n};\r\n\r\nexport function normalizeSelectorKey(selectorText: string): string {\r\n const text = selectorText.trim();\r\n // At-rule headers (@media, @keyframes, @supports...) are matched\r\n // whitespace-insensitive because CSSOM reformats them unpredictably.\r\n if (text.startsWith(\"@\")) return text.replace(/\\s+/g, \"\");\r\n return text\r\n .replace(/\\s*([>+~,])\\s*/g, \"$1\") // tighten combinators and selector lists\r\n .replace(/\\s+/g, \" \") // collapse descendant-combinator whitespace\r\n .replace(/\\(\\s*odd\\s*\\)/g, \"(2n+1)\") // CSSOM serializes :nth-child(odd) as (2n+1)\r\n .replace(/\\(\\s*even\\s*\\)/g, \"(2n)\")\r\n .trim();\r\n}\r\n\r\nexport function collectCSSRules(rules: CSSRuleList, map: Map<string, CSSRule>): Map<string, CSSRule> {\r\n for (let i = 0; i < rules.length; i++) {\r\n const rule = rules[i] as any;\r\n let key: string | null = null;\r\n if (typeof rule.selectorText === \"string\") {\r\n key = normalizeSelectorKey(rule.selectorText);\r\n } else if (typeof rule.cssText === \"string\" && rule.cssText.startsWith(\"@\")) {\r\n key = normalizeSelectorKey(rule.cssText.split(\"{\")[0]);\r\n }\r\n if (key && !map.has(key)) map.set(key, rule as CSSRule);\r\n }\r\n return map;\r\n}\r\n\r\nexport function ensureDomStyle(styleParent: HTMLHeadElement | ShadowRoot): HTMLStyleElement {\r\n let domStyle = styleParent.querySelector(\"#domphy-style\") as HTMLStyleElement | null;\r\n\r\n if (!domStyle) {\r\n domStyle = document.createElement(\"style\");\r\n domStyle.id = \"domphy-style\";\r\n styleParent.appendChild(domStyle);\r\n }\r\n\r\n if (domStyle.dataset.domphyBase !== \"true\") {\r\n domStyle.sheet?.insertRule(\"[hidden] { display: none !important; }\", 0);\r\n domStyle.dataset.domphyBase = \"true\";\r\n }\r\n\r\n return domStyle;\r\n}\r\n\r\nexport const mergePartial = (partial: PartialElement | DomphyElement): typeof partial => {\r\n\r\n if (Array.isArray(partial.$)) {\r\n let part: typeof partial = {}\r\n partial.$.forEach(p => merge(part, mergePartial(p)))\r\n delete partial.$\r\n merge(part, partial) // native win\r\n\r\n return part\r\n } else {\r\n return partial\r\n }\r\n}\r\n","export const VoidTags = [\r\n \"area\",\r\n \"base\",\r\n \"br\",\r\n \"col\",\r\n \"embed\",\r\n \"hr\",\r\n \"img\",\r\n \"input\",\r\n \"link\",\r\n \"meta\",\r\n \"source\",\r\n \"track\",\r\n \"wbr\",\r\n] as const;\r\n\r\nexport type VoidTagName = (typeof VoidTags)[number];\r\n\r\n","export const SvgTags = [\"svg\", \"circle\", \"path\", \"rect\", \"ellipse\",\r\n \"line\", \"polyline\", \"polygon\", \"g\", \"defs\",\r\n \"use\", \"symbol\", \"linearGradient\", \"radialGradient\",\r\n \"stop\", \"clipPath\", \"mask\", \"filter\", \"text\",\r\n \"tspan\", \"textPath\", \"image\", \"pattern\", \"marker\",\r\n \"animate\", \"animateTransform\", \"animateMotion\",\r\n \"feGaussianBlur\", \"feComposite\", \"feColorMatrix\",\r\n \"feMerge\", \"feMergeNode\", \"feOffset\", \"feFlood\",\r\n \"feBlend\", \"foreignObject\"]","export const BooleanAttributes = [\r\n \"allowFullScreen\",\r\n \"async\",\r\n \"autoFocus\",\r\n \"autoPlay\",\r\n \"checked\",\r\n \"compact\",\r\n \"contentEditable\",\r\n \"controls\",\r\n \"declare\",\r\n \"default\",\r\n \"defer\",\r\n \"disabled\",\r\n \"formNoValidate\",\r\n \"hidden\",\r\n \"isMap\",\r\n \"itemScope\",\r\n \"loop\",\r\n \"multiple\",\r\n \"muted\",\r\n \"noHref\",\r\n \"noShade\",\r\n \"noValidate\",\r\n \"open\",\r\n \"playsInline\",\r\n \"readonly\",\r\n \"required\",\r\n \"reversed\",\r\n \"scoped\",\r\n \"selected\",\r\n \"sortable\",\r\n \"trueSpeed\",\r\n \"typeMustMatch\",\r\n \"wmode\",\r\n \"autoCapitalize\",\r\n \"translate\",\r\n \"spellCheck\",\r\n \"inert\",\r\n \"download\",\r\n \"noModule\",\r\n \"paused\",\r\n \"autoPictureInPicture\",\r\n] as const;","//browserslist query \"> 0.1%, not dead\"\r\nexport const PrefixCSS: Record<string, string[]> = {\r\n transform: [\"webkit\", \"ms\"],\r\n transition: [\"webkit\", \"ms\"],\r\n animation: [\"webkit\"],\r\n userSelect: [\"webkit\", \"ms\"],\r\n flexDirection: [\"webkit\", \"ms\"],\r\n flexWrap: [\"webkit\", \"ms\"],\r\n justifyContent: [\"webkit\", \"ms\"],\r\n alignItems: [\"webkit\", \"ms\"],\r\n alignSelf: [\"webkit\", \"ms\"],\r\n order: [\"webkit\", \"ms\"],\r\n flexGrow: [\"webkit\", \"ms\"],\r\n flexShrink: [\"webkit\", \"ms\"],\r\n flexBasis: [\"webkit\", \"ms\"],\r\n columns: [\"webkit\"],\r\n columnCount: [\"webkit\"],\r\n columnGap: [\"webkit\"],\r\n columnRule: [\"webkit\"],\r\n columnWidth: [\"webkit\"],\r\n boxSizing: [\"webkit\"],\r\n appearance: [\"webkit\", \"moz\"],\r\n filter: [\"webkit\"],\r\n backdropFilter: [\"webkit\"],\r\n clipPath: [\"webkit\"],\r\n mask: [\"webkit\"],\r\n maskImage: [\"webkit\"],\r\n textSizeAdjust: [\"webkit\", \"ms\"],\r\n hyphens: [\"webkit\", \"ms\"],\r\n writingMode: [\"webkit\", \"ms\"],\r\n gridTemplateColumns: [\"ms\"],\r\n gridTemplateRows: [\"ms\"],\r\n gridAutoColumns: [\"ms\"],\r\n gridAutoRows: [\"ms\"],\r\n gridColumn: [\"ms\"],\r\n gridRow: [\"ms\"],\r\n marginInlineStart: [\"webkit\"],\r\n marginInlineEnd: [\"webkit\"],\r\n paddingInlineStart: [\"webkit\"],\r\n paddingInlineEnd: [\"webkit\"],\r\n minInlineSize: [\"webkit\"],\r\n maxInlineSize: [\"webkit\"],\r\n minBlockSize: [\"webkit\"],\r\n maxBlockSize: [\"webkit\"],\r\n inlineSize: [\"webkit\"],\r\n blockSize: [\"webkit\"],\r\n tabSize: [\"moz\"],\r\n overscrollBehavior: [\"webkit\", \"ms\"],\r\n touchAction: [\"ms\"],\r\n resize: [\"webkit\"],\r\n printColorAdjust: [\"webkit\"],\r\n backgroundClip: [\"webkit\"],\r\n boxDecorationBreak: [\"webkit\"],\r\n overflowScrolling: [\"webkit\"],\r\n};\r\n","export const CamelAttributes: string[] = [\r\n \"viewBox\",\r\n \"preserveAspectRatio\",\r\n \"gradientTransform\",\r\n \"gradientUnits\",\r\n \"spreadMethod\",\r\n \"markerStart\",\r\n \"markerMid\",\r\n \"markerEnd\",\r\n \"markerHeight\",\r\n \"markerWidth\",\r\n \"markerUnits\",\r\n \"refX\",\r\n \"refY\",\r\n \"patternContentUnits\",\r\n \"patternTransform\",\r\n \"patternUnits\",\r\n \"filterUnits\",\r\n \"primitiveUnits\",\r\n \"kernelUnitLength\",\r\n \"clipPathUnits\",\r\n \"maskContentUnits\",\r\n \"maskUnits\"\r\n] as const\r\n","import { escapeHTML, camelToKebab } from \"../helpers.js\";\nimport { BooleanAttributes, CamelAttributes } from \"../constants.js\";\nimport type { ElementNode } from \"./ElementNode.js\"\nimport { type AttributeValue } from \"../types.js\"\nimport { Notifier } from \"./Notifier.js\"\n\nexport class ElementAttribute {\n readonly name: string;\n readonly isBoolean: boolean;\n value: any;\n parent: ElementNode;\n _notifier = new Notifier();\n // Release handles for the reactive listener's state subscriptions, so a\n // re-set (e.g. patch() replacing a reactive value) can drop the old listener\n // instead of leaking it on the long-lived State until node removal.\n private _releases: (() => void)[] = [];\n\n constructor(name: string, value: any, parent: any) {\n this.parent = parent;\n this.isBoolean = (BooleanAttributes as readonly string[]).includes(name);\n if (CamelAttributes.includes(name)) {\n this.name = name\n } else {\n this.name = camelToKebab(name)\n }\n this.value = undefined;\n this.set(value);\n }\n\n render(): void {\n if (!this.parent || !this.parent.domElement) return;\n const domElement = this.parent.domElement;\n\n const mutateAttrs = [\"value\"];\n if (this.isBoolean) {\n if (this.value === false || this.value == null) {\n domElement.removeAttribute(this.name)\n } else {\n domElement.setAttribute(this.name, this.value === true ? \"\" : this.value)\n }\n } else if (this.value == null) {\n domElement.removeAttribute(this.name);\n } else if (mutateAttrs.includes(this.name)) {\n (domElement as any)[this.name] = this.value;\n } else {\n domElement.setAttribute(this.name, this.value);\n }\n }\n\n set(value: AttributeValue): void {\n let prev = this.value;\n\n // Drop any previous reactive subscription before (re)binding.\n if (this._releases.length) {\n for (const release of this._releases) release();\n this._releases = [];\n }\n\n if (value == null) {\n this.value = null;\n } else if (typeof value == \"function\") {\n let listener: any = () => {\n if (!this.parent || this.parent._disposed) return;\n let p = this.value;\n // Re-pass `listener` so states read only on a later run (conditional\n // dependencies) get subscribed too — matching children/style paths.\n this.value = this.isBoolean ? Boolean((value as Function)(listener)) : (value as Function)(listener);\n this.render();\n if (p !== this.value) this._notifier.notify(this.name, this.value);\n };\n\n listener.elementNode = this.parent!;\n listener.debug = `class:${this.parent?.tagName}_${this.parent?.nodeId} attribute:${this.name}`;\n\n listener.onSubscribe = (release: () => void) => {\n this._releases.push(release);\n if (this.parent) {\n this.parent.addHook(\"BeforeRemove\", () => {\n release();\n listener = null;\n });\n }\n };\n\n this.value = this.isBoolean ? Boolean(value(listener)) : value(listener);\n } else {\n this.value = this.isBoolean ? Boolean(value) : value;\n }\n\n this.render();\n if (prev !== this.value) this._notifier.notify(this.name, this.value);\n }\n\n addListener(callback: (value: any) => void): void {\n const handler = callback as any\n handler.onSubscribe = (release: () => void) => this.parent?.addHook(\"BeforeRemove\", release);\n this._notifier.addListener(this.name, handler)\n }\n\n remove(): void {\n if (this.parent && this.parent.attributes) {\n this.parent.attributes.remove(this.name);\n }\n this._dispose();\n }\n\n _dispose(): void {\n this._notifier._dispose();\n this.value = null;\n this.parent = null as any\n }\n\n generateHTML(): string {\n const { name, value } = this;\n if (this.isBoolean) {\n return value ? `${name}` : \"\";\n } else {\n const val = Array.isArray(value) ? JSON.stringify(value) : value;\n return `${name}=\"${escapeHTML(String(val))}\"`;\n }\n }\n}\n","import type { ElementNode } from \"./ElementNode.js\";\nimport { ElementAttribute } from \"./ElementAttribute.js\";\nimport { BooleanAttributes } from \"../constants.js\";\nimport { AttributeValue } from \"../types.js\"\n\nexport class AttributeList {\n items: Record<string, ElementAttribute> | null = {};\n parent: ElementNode | null;\n\n constructor(parent: ElementNode) {\n this.parent = parent;\n }\n\n generateHTML(): string {\n if (!this.items) return \"\";\n const str = Object.values(this.items)\n .map((attr) => attr.generateHTML())\n .join(\" \");\n return str ? ` ${str}` : \"\";\n }\n\n get(name: string): any {\n if (!this.items) return undefined;\n return this.items[name]?.value;\n }\n\n set(name: string, value: AttributeValue): void {\n if (!this.items || !this.parent) return;\n if (this.items[name]) {\n this.items[name].set(value);\n } else {\n this.items[name] = new ElementAttribute(name, value, this.parent);\n }\n }\n\n addListener(name: string, callback: (value: string | number) => void): void {\n if (this.has(name)) {\n this.items![name].addListener(callback);\n }\n }\n\n has(name: string): boolean {\n if (!this.items) return false;\n return Object.prototype.hasOwnProperty.call(this.items, name);\n }\n\n remove(name: string): void {\n if (!this.items) return;\n\n if (this.items[name]) {\n this.items[name]._dispose();\n delete this.items[name];\n }\n\n if (this.parent && this.parent.domElement && this.parent.domElement instanceof Element) {\n this.parent.domElement.removeAttribute(name);\n }\n }\n\n _dispose(): void {\n if (this.items) {\n for (const key in this.items) {\n this.items[key]._dispose();\n }\n }\n this.items = null;\n this.parent = null;\n }\n\n toggle(name: string, force?: boolean): void {\n if (\n !BooleanAttributes.includes(name as (typeof BooleanAttributes)[number])\n ) {\n throw Error(`${name} is not a boolean attribute`);\n }\n if (force === true) {\n this.set(name, true);\n } else if (force === false) {\n this.remove(name);\n } else {\n this.has(name) ? this.remove(name) : this.set(name, true);\n }\n }\n\n addClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n\n const add = (classes: string, newClass: string) => {\n const list = (classes || \"\").split(\" \").filter((e: string) => e);\n !list.includes(newClass) && list.push(className)\n return list.join(\" \")\n }\n\n let current = this.get(\"class\");\n\n if (typeof current === \"function\") {\n this.set(\"class\", () => add(current(), className));\n } else {\n this.set(\"class\", add(current, className))\n }\n }\n\n hasClass(className: string): boolean {\n if (!className || typeof className !== \"string\") return false;\n const current = this.get(\"class\") || \"\";\n const list = current.split(\" \").filter((e: string) => e);\n return list.includes(className);\n }\n\n toggleClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n this.hasClass(className)\n ? this.removeClass(className)\n : this.addClass(className);\n }\n\n removeClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n const current = this.get(\"class\") || \"\";\n const list: string[] = current.split(\" \").filter((e: string) => e);\n const updated = list.filter((cls) => cls !== className);\n updated.length > 0\n ? this.set(\"class\", updated.join(\" \"))\n : this.remove(\"class\");\n }\n\n replaceClass(oldClass: string, newClass: string): void {\n if (\n !oldClass ||\n !newClass ||\n typeof oldClass !== \"string\" ||\n typeof newClass !== \"string\"\n )\n return;\n if (this.hasClass(oldClass)) {\n this.removeClass(oldClass);\n this.addClass(newClass);\n }\n }\n}\n","import { ElementNode } from \"./ElementNode.js\";\r\nimport { isHTML, escapeHTML } from \"../helpers.js\";\r\n\r\nexport class TextNode {\r\n type = \"TextNode\"\r\n parent: ElementNode;\r\n text: string;\r\n domText?: ChildNode;\r\n\r\n constructor(textContent: string | number, parent: ElementNode) {\r\n this.parent = parent;\r\n this.text = textContent === \"\" ? \"\\u200B\" : String(textContent);\r\n }\r\n _createDOMNode() {\r\n let newNode: ChildNode;\r\n if (isHTML(this.text)) {\r\n const tpl = document.createElement(\"template\");\r\n tpl.innerHTML = this.text.trim();\r\n newNode = tpl.content.firstChild || document.createTextNode(\"\");\r\n } else {\r\n newNode = document.createTextNode(this.text);\r\n }\r\n this.domText = newNode;\r\n return newNode;\r\n }\r\n\r\n _dispose(): void {\r\n this.domText = undefined;\r\n this.text = \"\";\r\n }\r\n\r\n generateHTML(): string {\r\n if (this.text === \"\\u200B\") return \"&#8203;\";\r\n // Mirror _createDOMNode: a single-root HTML string is intentional inline\r\n // HTML, anything else is plain text and must be escaped so the server\r\n // output is XSS-safe and parses back to the same text node the client\r\n // builds (otherwise hydration child alignment drifts).\r\n return isHTML(this.text) ? this.text : escapeHTML(this.text);\r\n }\r\n\r\n render(domText: ChildNode | DocumentFragment | HTMLElement): void {\r\n const newNode = this._createDOMNode();\r\n domText.appendChild(newNode);\r\n }\r\n}","import { TextNode } from \"./TextNode.js\";\r\nimport { ElementNode } from \"./ElementNode.js\";\r\nimport type { DomphyElement } from \"../types.js\";\r\nimport { ensureDomStyle, getTagName } from \"../helpers.js\";\r\n\r\ntype ElementInput = DomphyElement | null | undefined | number | string\r\ntype NodeItem = ElementNode | TextNode;\r\n\r\nexport class ElementList {\r\n items: NodeItem[] = [];\r\n owner: ElementNode;\r\n _nextKey: number = 0;\r\n\r\n constructor(parent: ElementNode) {\r\n this.owner = parent;\r\n }\r\n\r\n _createNode(element: ElementInput | DomphyElement): NodeItem {\r\n return (typeof element === \"object\" && element !== null)\r\n ? new ElementNode(element, this.owner, this._nextKey++)\r\n : new TextNode(element == null ? \"\" : String(element), this.owner);\r\n }\r\n\r\n _moveDomElement(node: NodeItem, index: number) {\r\n if (!this.owner || !this.owner.domElement) return;\r\n const dom = this.owner.domElement;\r\n\r\n const el = node instanceof ElementNode ? node.domElement : node.domText;\r\n if (el) {\r\n const currentRef = dom.childNodes[index] || null;\r\n if (el !== currentRef) {\r\n dom.insertBefore(el, currentRef);\r\n }\r\n }\r\n }\r\n\r\n _swapDomElement(aNode: NodeItem, bNode: NodeItem) {\r\n if (!this.owner || !this.owner.domElement) return;\r\n const parent = this.owner.domElement;\r\n\r\n const a = aNode instanceof ElementNode ? aNode.domElement : aNode.domText;\r\n const b = bNode instanceof ElementNode ? bNode.domElement : bNode.domText;\r\n if (!a || !b) return;\r\n\r\n const aNext = a.nextSibling;\r\n const bNext = b.nextSibling;\r\n\r\n parent.insertBefore(a, bNext);\r\n parent.insertBefore(b, aNext);\r\n }\r\n\r\n update(inputs: ElementInput[], updateDom = true, silent = false): void {\r\n\r\n const oldItems = this.items.slice(); // snapshot for cleanup\r\n\r\n // keyed lookup from old list\r\n const keyed = new Map<string | number, NodeItem>();\r\n for (const item of oldItems) {\r\n if (item instanceof ElementNode && item.key !== null && item.key !== undefined) {\r\n keyed.set(item.key, item);\r\n }\r\n }\r\n\r\n if (!silent && this.owner.domElement) this.owner._hooks?.BeforeUpdate?.(this.owner, inputs);\r\n\r\n const oldSet = new Set<NodeItem>(oldItems);\r\n const claimed = new Set<NodeItem>();\r\n\r\n // build target order using existing ops (mutating this.items)\r\n for (let i = 0; i < inputs.length; i++) {\r\n const input = inputs[i];\r\n const isObj = typeof input === \"object\" && input !== null;\r\n const key = isObj ? (input as any)._key : undefined;\r\n const tag = isObj ? getTagName(input as DomphyElement) : undefined;\r\n\r\n // Keyed reuse: same key + same tag → reuse the node and patch it in place\r\n // (preserves DOM identity/state while reflecting new data).\r\n if (key !== undefined) {\r\n const reused = keyed.get(key);\r\n if (reused instanceof ElementNode && reused.tagName === tag) {\r\n keyed.delete(key);\r\n const cur = this.items.indexOf(reused);\r\n if (cur !== i && cur >= 0) {\r\n const isPortal = !!reused._portal;\r\n this.move(cur, i, isPortal ? false : updateDom, true);\r\n }\r\n reused.parent = this.owner as any;\r\n reused.patch(input as DomphyElement);\r\n claimed.add(reused);\r\n continue;\r\n }\r\n // key present but no tag-compatible match → fall through to insert; any\r\n // stale keyed node keeps its slot in `keyed` and is removed below.\r\n } else if (isObj) {\r\n // Unkeyed positional reuse: reuse the old unkeyed element already sitting\r\n // at this slot if its tag matches — this is what preserves focus, scroll,\r\n // selection, IME and uncontrolled input values across plain list updates.\r\n const at = this.items[i];\r\n if (at instanceof ElementNode && at.key == null && at.tagName === tag\r\n && oldSet.has(at) && !claimed.has(at)) {\r\n at.parent = this.owner as any;\r\n at.patch(input as DomphyElement);\r\n claimed.add(at);\r\n continue;\r\n }\r\n }\r\n\r\n claimed.add(this.insert(input, i, updateDom, true));\r\n }\r\n\r\n // Remove leftover nodes beyond the new length. Iterate a SNAPSHOT (not a\r\n // `while length > inputs.length` loop): a removal may defer (async exit\r\n // animation), leaving the node in `items`, so a length-based loop would spin.\r\n const extras = this.items.slice(inputs.length);\r\n for (const node of extras) this.remove(node, updateDom, true);\r\n keyed.forEach((node) => this.remove(node, updateDom, true));\r\n if (!silent) this.owner._hooks?.Update?.(this.owner);\r\n }\r\n\r\n insert(input: ElementInput, index?: number, updateDom = true, silent = false): NodeItem {\r\n\r\n let length = this.items.length;\r\n const finalIndex = (typeof index !== \"number\" || isNaN(index) || index < 0 || index > length)\r\n ? length\r\n : index;\r\n const item = this._createNode(input);\r\n this.items.splice(finalIndex, 0, item);\r\n\r\n if (item instanceof ElementNode) {\r\n //Parent always insert/mount before children\r\n item._hooks.Insert && item._hooks.Insert(item)\r\n\r\n let domElement = this.owner.domElement;\r\n if (updateDom && domElement) {\r\n\r\n\r\n if (item._portal) {\r\n let domElement = item._portal!(this.owner.getRoot())\r\n domElement && item.render(domElement)\r\n } else {\r\n let domNode = item._createDOMNode();\r\n const ref = domElement.childNodes[finalIndex] ?? null;\r\n domElement.insertBefore(domNode, ref);\r\n let root = domElement.getRootNode()\r\n const styleParent = root instanceof ShadowRoot ? root : document.head\r\n let domStyle = ensureDomStyle(styleParent)\r\n item.styles.render(domStyle as HTMLStyleElement)\r\n item._hooks.Mount && item._hooks.Mount(item)\r\n item.children.items.forEach(child => {\r\n if (child instanceof ElementNode && child._portal) {\r\n let dom = child._portal!(child.getRoot())\r\n dom && child.render(dom)\r\n } else {\r\n child.render(domNode)\r\n }\r\n })\r\n }\r\n }\r\n\r\n\r\n\r\n } else {\r\n let domElement = this.owner.domElement;\r\n if (updateDom && domElement) {\r\n let domNode = item._createDOMNode();\r\n const ref = domElement.childNodes[finalIndex] ?? null;\r\n domElement.insertBefore(domNode, ref);\r\n }\r\n }\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n return item;\r\n }\r\n\r\n remove(item: NodeItem, updateDom = true, silent = false): void {\r\n\r\n const index = this.items.indexOf(item);\r\n if (index < 0) return;\r\n\r\n if (item instanceof ElementNode) {\r\n // Guard against re-entrant removal of a node whose (deferred) removal is\r\n // already in flight — otherwise update()'s extras + keyed passes could\r\n // fire its BeforeRemove/animation twice. Synchronous removals are already\r\n // guarded by the indexOf check above (the node is spliced before re-entry).\r\n if (item._beforeRemoveFired) return;\r\n const done = () => {\r\n const el = item.domElement\r\n // Re-resolve position at completion time — a deferred (animated) removal\r\n // may run after other inserts/removes have shifted indices.\r\n const i = this.items.indexOf(item);\r\n if (i >= 0) this.items.splice(i, 1);\r\n updateDom && el && el.remove()\r\n item._dispose(); // _dispose fires Remove + releases subscriptions for the whole subtree\r\n }\r\n if (item._hooks.BeforeRemove && item.domElement) {\r\n let doneCalled = false;\r\n const onceDone = () => { if (!doneCalled) { doneCalled = true; done(); } };\r\n item._beforeRemoveFired = true; // prevent _dispose from re-firing BeforeRemove\r\n item._hooks.BeforeRemove(item, onceDone);\r\n // Auto-complete only for sync cleanup hooks. A hook that declares `done`\r\n // (arity >= 2, e.g. an exit animation) owns completion and defers removal.\r\n if ((item._hooks.BeforeRemove as Function).length < 2 && !doneCalled) onceDone();\r\n } else {\r\n done()\r\n }\r\n\r\n } else {\r\n const el = item.domText\r\n this.items.splice(index, 1);\r\n updateDom && el && el.remove()\r\n item._dispose();\r\n }\r\n\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n }\r\n\r\n clear(updateDom = true, silent = false): void {\r\n if (this.items.length === 0) return;\r\n const snapshot = this.items.slice();\r\n\r\n for (const item of snapshot) {\r\n this.remove(item, updateDom, true);\r\n }\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n }\r\n\r\n _dispose(): void {\r\n this.items.forEach(child => child._dispose())\r\n this.items = [];\r\n }\r\n\r\n swap(aIndex: number, bIndex: number, updateDom = true, silent = false) {\r\n if (aIndex < 0 || bIndex < 0 ||\r\n aIndex >= this.items.length || bIndex >= this.items.length ||\r\n aIndex === bIndex) return;\r\n\r\n const itemA = this.items[aIndex];\r\n const itemB = this.items[bIndex];\r\n\r\n this.items[aIndex] = itemB;\r\n this.items[bIndex] = itemA;\r\n\r\n if (updateDom) this._swapDomElement(itemA, itemB);\r\n\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n }\r\n\r\n move(fromIndex: number, toIndex: number, updateDom = true, silent = false): void {\r\n if (fromIndex < 0 || fromIndex >= this.items.length ||\r\n toIndex < 0 || toIndex >= this.items.length || fromIndex === toIndex) return;\r\n\r\n const item = this.items[fromIndex];\r\n\r\n this.items.splice(fromIndex, 1);\r\n this.items.splice(toIndex, 0, item);\r\n\r\n if (updateDom) this._moveDomElement(item, toIndex);\r\n\r\n !silent && this.owner.domElement && this.owner._hooks.Update && this.owner._hooks.Update(this.owner)\r\n }\r\n\r\n generateHTML(): string {\r\n let html = \"\";\r\n for (const item of this.items) html += item.generateHTML();\r\n return html;\r\n }\r\n}\r\n","import type { StyleRule } from \"./StyleRule.js\";\r\nimport type { StyleValue } from \"../types.js\";\r\nimport { camelToKebab } from \"../helpers.js\";\r\nimport { PrefixCSS } from \"../constants.js\";\r\nimport { Listener } from \"../types.js\"\r\n\r\nexport class StyleProperty {\r\n name: string;\r\n cssName: string;\r\n value: StyleValue = \"\";\r\n parentRule: StyleRule;\r\n\r\n constructor(name: string, value: StyleValue, parentRule: StyleRule) {\r\n this.name = name;\r\n this.cssName = camelToKebab(name);\r\n this.parentRule = parentRule;\r\n this.set(value);\r\n }\r\n\r\n _domUpdate(): void {\r\n if (!this.parentRule) return;\r\n const domRule = this.parentRule.domRule;\r\n\r\n if (domRule && (domRule as CSSStyleRule).style) {\r\n let style: CSSStyleDeclaration = (domRule as CSSStyleRule).style;\r\n style.setProperty(this.cssName, String(this.value));\r\n\r\n if (PrefixCSS[this.name]) {\r\n PrefixCSS[this.name].forEach((prefix) => {\r\n style.setProperty(`-${prefix}-${this.cssName}`, String(this.value));\r\n });\r\n }\r\n }\r\n }\r\n _dispose(): void {\r\n this.value = \"\";\r\n this.parentRule = null as any;\r\n }\r\n\r\n set(value: StyleValue): void {\r\n\r\n if (typeof value === \"function\") {\r\n let listener = (() => {\r\n if (!this.parentRule || this.parentRule.parentNode?._disposed) return;\r\n this.value = value(listener);\r\n this._domUpdate();\r\n }) as unknown as Listener;\r\n\r\n listener.onSubscribe = (release: () => void) => {\r\n this.parentRule.parentNode?.addHook(\"BeforeRemove\", () => {\r\n release();\r\n listener = null!;\r\n });\r\n };\r\n\r\n listener.elementNode = this.parentRule!.root!;\r\n listener.debug = `class:${this.parentRule?.root?.tagName}_${this.parentRule?.root?.nodeId} style:${this.name}`;\r\n this.value = value(listener);\r\n } else {\r\n this.value = value;\r\n }\r\n\r\n this._domUpdate();\r\n }\r\n\r\n remove(): void {\r\n if (!this.parentRule) return;\r\n\r\n if (this.parentRule.domRule instanceof CSSStyleRule) {\r\n const domStyle = this.parentRule.domRule.style;\r\n domStyle.removeProperty(this.cssName);\r\n\r\n if (PrefixCSS[this.name]) {\r\n PrefixCSS[this.name].forEach((prefix) => {\r\n domStyle.removeProperty(`-${prefix}-${this.cssName}`);\r\n });\r\n }\r\n }\r\n delete this.parentRule.styleBlock![this.name];\r\n this._dispose();\r\n }\r\n\r\n cssText(): string {\r\n let str = `${this.cssName}: ${this.value}`;\r\n if (PrefixCSS[this.name]) {\r\n PrefixCSS[this.name].forEach((prefix) => {\r\n str += `; -${prefix}-${this.cssName}: ${this.value}`;\r\n });\r\n }\r\n return str;\r\n }\r\n}","import { ElementNode } from \"./ElementNode.js\";\r\nimport { StyleProperty } from \"./StyleProperty.js\";\r\nimport { StyleList } from \"./StyleList.js\";\r\n\r\nexport class StyleRule {\r\n selectorText: string;\r\n domRule: CSSRule | CSSMediaRule | CSSKeyframesRule | null = null;\r\n styleList: StyleList | null;\r\n styleBlock: Record<string, StyleProperty> | null = {};\r\n parent: StyleRule | ElementNode | null;\r\n\r\n constructor(selectorText: string, parent: StyleRule | ElementNode) {\r\n this.selectorText = selectorText;\r\n this.styleList = new StyleList(this);\r\n this.parent = parent;\r\n }\r\n\r\n _dispose(): void {\r\n\r\n if (this.styleBlock) {\r\n for (const prop of Object.values(this.styleBlock)) {\r\n prop._dispose();\r\n }\r\n }\r\n\r\n if (this.styleList) {\r\n this.styleList._dispose();\r\n }\r\n\r\n this.styleBlock = null;\r\n this.styleList = null;\r\n this.domRule = null;\r\n this.parent = null;\r\n }\r\n\r\n get root() {\r\n let node = this.parent;\r\n while (node instanceof StyleRule) {\r\n node = node.parent;\r\n }\r\n return node;\r\n }\r\n\r\n get parentNode(): ElementNode | null {\r\n let root: any = this.parent;\r\n while (root && root instanceof StyleRule) {\r\n root = root.parent;\r\n }\r\n return root as ElementNode;\r\n }\r\n \r\n insertStyle(name: string, val: any): void {\r\n if (!this.styleBlock) return;\r\n if (this.styleBlock[name]) {\r\n this.styleBlock[name].set(val);\r\n } else {\r\n this.styleBlock[name] = new StyleProperty(name, val, this);\r\n }\r\n }\r\n\r\n removeStyle(name: string): void {\r\n if (!this.styleBlock) return;\r\n if (this.styleBlock[name]) {\r\n this.styleBlock[name].remove();\r\n }\r\n }\r\n\r\n cssText(): string {\r\n if (!this.styleBlock || !this.styleList) return \"\";\r\n const styleStr = Object.values(this.styleBlock).map(decl => decl.cssText()).join(\";\");\r\n const nested = this.styleList.cssText();\r\n return `${this.selectorText} { ${styleStr} ${nested} } `;\r\n }\r\n\r\n mount(domRule: CSSRule | CSSKeyframesRule): void {\r\n if (!domRule || !this.styleList) return;\r\n this.domRule = domRule;\r\n if (\"cssRules\" in domRule) {\r\n this.styleList.mount(domRule.cssRules as CSSRuleList);\r\n }\r\n }\r\n\r\n remove(): void {\r\n\r\n if (this.domRule && this.domRule.parentStyleSheet) {\r\n const sheet = this.domRule.parentStyleSheet;\r\n const rules = sheet.cssRules;\r\n for (let i = 0; i < rules.length; i++) {\r\n if (rules[i] === this.domRule) {\r\n sheet.deleteRule(i);\r\n break;\r\n }\r\n }\r\n }\r\n this._dispose();\r\n }\r\n\r\n render(domSheet: CSSStyleSheet | CSSGroupingRule) {\r\n if (!this.styleBlock || !this.styleList) return;\r\n const styleStr = Object.values(this.styleBlock).map(decl => decl.cssText()).join(\";\");\r\n try {\r\n if (!this.selectorText.startsWith(\"@\")) {\r\n const css = `${this.selectorText} { ${styleStr} }`;\r\n const index = domSheet.insertRule(css, domSheet.cssRules.length);\r\n const domRule = domSheet.cssRules[index];\r\n if (domRule && \"selectorText\" in domRule) {\r\n this.mount(domRule);\r\n }\r\n } else if (/^@(media|supports|container|layer)\\b/.test(this.selectorText)) {\r\n const index = domSheet.insertRule(`${this.selectorText} {}`, domSheet.cssRules.length);\r\n const domRule = domSheet.cssRules[index];\r\n if (\"cssRules\" in domRule) {\r\n this.mount(domRule as CSSGroupingRule);\r\n this.styleList.render(domRule as CSSGroupingRule);\r\n }\r\n } else if (this.selectorText.startsWith(\"@keyframes\") || this.selectorText.startsWith(\"@font-face\")) {\r\n const css = this.cssText();\r\n const index = domSheet.insertRule(css, domSheet.cssRules.length);\r\n const domRule = domSheet.cssRules[index];\r\n this.mount(domRule);\r\n }\r\n } catch (err) {\r\n console.warn(\"Failed to insert rule:\", this.selectorText, err);\r\n }\r\n }\r\n}","import { ElementNode } from \"./ElementNode.js\";\r\nimport { selectorSplitter, normalizeSelectorKey } from \"../helpers.js\";\r\nimport { StyleRule } from \"./StyleRule.js\";\r\n\r\nexport class StyleList {\r\n parent: StyleRule | ElementNode | null;\r\n items: StyleRule[] = [];\r\n domStyle: HTMLStyleElement | null = null;\r\n\r\n constructor(parent: StyleRule | ElementNode) {\r\n this.parent = parent;\r\n }\r\n\r\n get parentNode(): ElementNode | null {\r\n let root: any = this.parent;\r\n while (root && root instanceof StyleRule) {\r\n root = root.parent;\r\n }\r\n return root as ElementNode;\r\n }\r\n\r\n addCSS(obj: Record<string, any>, parentSelector: string = \"\"): void {\r\n if (!this.items || !this.parent) return;\r\n const basic: Record<string, any> = {};\r\n\r\n function getSelector(selector: string, prev: string): string {\r\n return selector.startsWith(\"&\")\r\n ? `${prev}${selector.slice(1)}`\r\n : `${prev} ${selector}`;\r\n }\r\n\r\n for (const selector in obj) {\r\n const value = obj[selector];\r\n let splitKeys = selectorSplitter(selector);\r\n for (let key of splitKeys) {\r\n const currentSelector = getSelector(key, parentSelector);\r\n if (/^@(container|layer|supports|media)\\b/.test(key)) {\r\n if (typeof value === \"object\" && value != null) {\r\n const rule = new StyleRule(key, this.parent);\r\n rule.styleList!.addCSS(value, parentSelector);\r\n this.items.push(rule);\r\n }\r\n } else if (key.startsWith(\"@keyframes\")) {\r\n const rule = new StyleRule(key, this.parent);\r\n rule.styleList!.addCSS(value, \"\");\r\n this.items.push(rule);\r\n } else if (key.startsWith(\"@font-face\")) {\r\n const rule = new StyleRule(key, this.parent);\r\n for (const k in value) rule.insertStyle(k, value[k]);\r\n this.items.push(rule);\r\n } else if (typeof value === \"object\" && value != null) {\r\n const rule = new StyleRule(currentSelector, this.parent);\r\n this.items.push(rule);\r\n for (const [k, v] of Object.entries(value)) {\r\n if (typeof v === \"object\" && v != null) {\r\n let newSelector = getSelector(k, currentSelector);\r\n if (k.startsWith(\"&\")) {\r\n this.addCSS(v, newSelector);\r\n } else {\r\n const r = rule.styleList!.insertRule(newSelector);\r\n r.styleList!.addCSS(v, newSelector);\r\n }\r\n } else {\r\n rule.insertStyle(k, v);\r\n }\r\n }\r\n } else {\r\n basic[key] = value;\r\n }\r\n }\r\n }\r\n\r\n if (Object.keys(basic).length) {\r\n const rule = new StyleRule(parentSelector, this.parent);\r\n for (const key in basic) rule.insertStyle(key, basic[key]);\r\n this.items.push(rule);\r\n }\r\n }\r\n\r\n cssText(): string {\r\n if (!this.items) return \"\";\r\n return this.items.map((rule) => rule.cssText()).join(\"\");\r\n }\r\n\r\n insertRule(selector: string): StyleRule {\r\n if (!this.items || !this.parent) return null as any;\r\n let rule = this.items.find((rule) => rule.selectorText === selector);\r\n if (!rule) {\r\n rule = new StyleRule(selector, this.parent);\r\n this.items.push(rule);\r\n }\r\n return rule;\r\n }\r\n\r\n hydrate(domRuleMap: Map<string, CSSRule>): void {\r\n if (!this.items) return;\r\n for (const rule of this.items) {\r\n const domRule = domRuleMap.get(normalizeSelectorKey(rule.selectorText));\r\n if (domRule) rule.mount(domRule as CSSRule);\r\n }\r\n }\r\n\r\n mount(domRuleList: CSSRuleList): void {\r\n if (!this.items) return;\r\n if (!domRuleList) throw Error(\"Require domRuleList argument\");\r\n let wrongCount = 0;\r\n const fixOddEven = (css: string) => css.replace(\"(odd)\", \"(2n+1)\").replace(\"(even)\", \"(2n)\");\r\n\r\n this.items.forEach((rule, i) => {\r\n const index = i - wrongCount;\r\n const domRule = domRuleList[index];\r\n if (!domRule) return;\r\n if (rule.selectorText.startsWith(\"@\") && domRule instanceof CSSKeyframesRule) {\r\n rule.mount(domRule);\r\n } else if (\"keyText\" in domRule) {\r\n rule.mount(domRule);\r\n } else if (\"selectorText\" in domRule) {\r\n if (domRule.selectorText !== fixOddEven(rule.selectorText)) {\r\n wrongCount += 1;\r\n } else {\r\n rule.mount(domRule);\r\n }\r\n } else if (\"cssRules\" in domRule) {\r\n rule.mount(domRule as CSSMediaRule);\r\n }\r\n });\r\n }\r\n\r\n render(dom: HTMLStyleElement | CSSGroupingRule) {\r\n if (dom instanceof HTMLStyleElement) {\r\n this.domStyle = dom\r\n this.items.forEach((rule) => rule.render(dom.sheet!));\r\n } else if (dom instanceof CSSGroupingRule) {\r\n this.items.forEach((rule) => rule.render(dom));\r\n }\r\n }\r\n\r\n _dispose(): void {\r\n\r\n if (this.items) {\r\n for (let i = 0; i < this.items.length; i++) {\r\n this.items[i]._dispose();\r\n }\r\n }\r\n\r\n this.items = [];\r\n this.parent = null;\r\n this.domStyle = null;\r\n }\r\n}\r\n","import type { DomphyElement, EventName, HookMap, TagName, PartialElement } from \"../types.js\";\r\nimport { AttributeList } from \"./AttributeList.js\";\r\nimport { ElementList } from \"./ElementList.js\";\r\nimport { StyleList } from \"./StyleList.js\";\r\nimport { validate, mergePartial, getTagName, deepClone, ensureDomStyle, collectCSSRules } from \"../helpers.js\";\r\nimport { merge, hashString } from \"../utils.js\";\r\nimport { SvgTags, VoidTags } from \"../constants.js\"\r\n\r\nexport class ElementNode {\r\n _disposed = false\r\n _beforeRemoveFired = false\r\n type = \"ElementNode\"\r\n parent: ElementNode | null = null;\r\n _portal?: (root: ElementNode) => HTMLElement;\r\n tagName: TagName;\r\n children = new ElementList(this);\r\n styles = new StyleList(this);\r\n attributes = new AttributeList(this);\r\n domElement?: HTMLElement | null = null;\r\n _hooks: HookMap = {};\r\n _events?: { [K in EventName]?: (event: Event, node: ElementNode) => void } | null = null;\r\n _boundEvents = new Set<EventName>();\r\n _context?: Record<string, any> = {};\r\n _metadata?: Record<string, any> = {};\r\n key?: string | number | null = null;\r\n nodeId: string\r\n\r\n constructor(domphyElement: DomphyElement, _parent: ElementNode | null = null, index = 0) {\r\n domphyElement = deepClone(domphyElement)\r\n validate(domphyElement)\r\n domphyElement.style = domphyElement.style || {}\r\n this.parent = _parent;\r\n this.tagName = getTagName(domphyElement) as TagName;\r\n domphyElement = mergePartial(domphyElement) as DomphyElement\r\n\r\n this.key = (domphyElement as any)._key ?? null;\r\n this._context = domphyElement._context || {}\r\n this._metadata = domphyElement._metadata || {}\r\n\r\n let tempPath = `${this.parent?.nodeId}.${index}`\r\n const str = JSON.stringify(domphyElement.style || {}, (k, v) => typeof v === \"function\" ? tempPath : v,);\r\n this.nodeId = hashString(tempPath + str)\r\n\r\n this.attributes!.addClass(`${this.tagName}_${this.nodeId}`);\r\n if (domphyElement._onSchedule) domphyElement._onSchedule(this, domphyElement)\r\n\r\n this.merge(domphyElement)\r\n\r\n const children = (domphyElement as any)[this.tagName];\r\n\r\n if (children != null && children != undefined) {\r\n if (typeof children === \"function\") {\r\n\r\n let listener: any = () => {\r\n if (this._disposed) return\r\n let input = children(listener)\r\n this.children!.update(Array.isArray(input) ? input : [input])\r\n }\r\n\r\n listener!.elementNode = this;\r\n listener!.debug = `class:${this.tagName}_${this.nodeId} children`;\r\n listener!.onSubscribe = (release: () => void) => this.addHook(\"BeforeRemove\", () => {\r\n release()\r\n listener = null\r\n });\r\n listener && listener();\r\n } else {\r\n this.children!.update(Array.isArray(children) ? children : [children])\r\n }\r\n }\r\n this._hooks.Init && this._hooks.Init(this)\r\n }\r\n\r\n _createDOMNode() {\r\n const svgNamespace = \"http://www.w3.org/2000/svg\"\r\n let node = SvgTags.includes(this.tagName)\r\n ? document.createElementNS(svgNamespace, this.tagName)\r\n : document.createElement(this.tagName)\r\n\r\n this.domElement = node as HTMLElement\r\n\r\n if (this._events) {\r\n for (const key in this._events) this._bindEvent(key as EventName)\r\n }\r\n\r\n if (this.attributes) {\r\n Object.values(this.attributes.items!).forEach(attr => attr.render())\r\n }\r\n return node\r\n }\r\n\r\n // Bind a DOM listener that dispatches LIVE from this._events, so patch() can\r\n // swap the handler (e.g. a list item's onClick closure after its data changes)\r\n // without detaching/reattaching the DOM listener.\r\n _bindEvent(eventName: EventName): void {\r\n if (!this.domElement || this._boundEvents.has(eventName)) return\r\n this._boundEvents.add(eventName)\r\n let fn: any = (event: Event) => this._events?.[eventName]?.(event, this)\r\n this.domElement.addEventListener(eventName, fn)\r\n this.addHook(\"BeforeRemove\", (n) => {\r\n n.domElement?.removeEventListener(eventName, fn)\r\n fn = null\r\n })\r\n }\r\n\r\n _dispose(): void {\r\n if (this._disposed) return\r\n this._disposed = true\r\n\r\n // Fire BeforeRemove so reactive-listener releases (registered as BeforeRemove\r\n // hooks via onSubscribe) actually run for this node. Descendants are torn\r\n // down through this recursive _dispose — not through ElementList.remove — so\r\n // without this their subscriptions to long-lived State/RecordState leak.\r\n // Skip if the async-removal path in ElementList already fired it.\r\n if (!this._beforeRemoveFired) {\r\n this._beforeRemoveFired = true\r\n this._hooks.BeforeRemove?.(this, () => { })\r\n }\r\n\r\n if (this.children) {\r\n this.children._dispose();\r\n }\r\n\r\n if (this.styles) {\r\n this.styles.items!.forEach((rule) => rule.remove());\r\n this.styles._dispose();\r\n }\r\n\r\n if (this.attributes) {\r\n this.attributes._dispose();\r\n }\r\n\r\n // _onRemove fires for every node in the subtree, not just the directly-removed one.\r\n this._hooks.Remove?.(this)\r\n\r\n this.domElement = null;\r\n this._hooks = {};\r\n this._events = null;\r\n this._context = {};\r\n this._metadata = {};\r\n this.parent = null;\r\n }\r\n merge(part: PartialElement) {\r\n merge(this._context, part._context)\r\n merge(this._metadata, part._metadata)\r\n\r\n const keys = Object.keys(part)\r\n for (let i = 0; i < keys.length; i++) {\r\n const originalKey = keys[i];\r\n const value = (part as any)[originalKey];\r\n if ([\"$\", \"_onSchedule\", \"_key\", \"_context\", \"_metadata\", \"style\", this.tagName].includes(originalKey)) {\r\n continue\r\n } else if ([\"_onInit\", \"_onInsert\", \"_onMount\", \"_onBeforeUpdate\", \"_onUpdate\", \"_onBeforeRemove\", \"_onRemove\"].includes(originalKey)) {\r\n this.addHook(originalKey.substring(3) as keyof HookMap, value);\r\n } else if (originalKey.startsWith(\"on\")) {\r\n this.addEvent(originalKey.substring(2).toLowerCase() as EventName, value);\r\n } else if (originalKey == \"_portal\") {\r\n this._portal = value\r\n } else if (originalKey == \"class\" && typeof value === \"string\") {\r\n this.attributes!.addClass(value);\r\n } else {\r\n this.attributes!.set(originalKey, value);\r\n }\r\n }\r\n if (part.style) {\r\n this.styles.addCSS(part.style || {}, `.${`${this.tagName}_${this.nodeId}`}`);\r\n }\r\n\r\n }\r\n\r\n // Update this live node IN PLACE from a fresh element description, preserving\r\n // its DOM element (and thus focus/scroll/selection/uncontrolled value) and its\r\n // children's identity. Used by list reconciliation to reuse a node by key\r\n // (keyed) or position (unkeyed) while reflecting new data, instead of\r\n // destroying and recreating the DOM. Styles and lifecycle hooks are NOT\r\n // re-applied (reused items share structure; hooks already ran). Reactive\r\n // content (a function child) keeps its own listener and is left untouched.\r\n patch(rawElement: DomphyElement): void {\r\n let element: any = deepClone(rawElement)\r\n element.style = element.style || {}\r\n element = mergePartial(element)\r\n\r\n // Children / content — recurse so grandchildren are reused/patched too.\r\n const content = element[this.tagName]\r\n if (typeof content !== \"function\") {\r\n const next = content == null ? [] : Array.isArray(content) ? content : [content]\r\n this.children.update(next, !!this.domElement, true)\r\n }\r\n\r\n if (element._context) merge(this._context, element._context)\r\n if (element._metadata) merge(this._metadata, element._metadata)\r\n\r\n // Rebuild attributes and events. Events are replaced (live dispatch in\r\n // _bindEvent reads this._events, so swapping the map is enough); attributes\r\n // present before but absent now are removed; the auto scope class is kept.\r\n const autoClass = `${this.tagName}_${this.nodeId}`\r\n const reserved = [\"$\", \"_onSchedule\", \"_key\", \"_context\", \"_metadata\", \"style\", this.tagName]\r\n const hookKeys = [\"_onInit\", \"_onInsert\", \"_onMount\", \"_onBeforeUpdate\", \"_onUpdate\", \"_onBeforeRemove\", \"_onRemove\"]\r\n const keep = new Set<string>([\"class\"])\r\n let userClass: string | null = null\r\n\r\n this._events = {}\r\n for (const key of Object.keys(element)) {\r\n if (reserved.includes(key) || hookKeys.includes(key) || key === \"_portal\") continue\r\n const value = element[key]\r\n if (key.startsWith(\"on\") && typeof value === \"function\") {\r\n this.addEvent(key.substring(2).toLowerCase() as EventName, value)\r\n } else if (key === \"class\" && typeof value === \"string\") {\r\n userClass = value\r\n } else {\r\n this.attributes!.set(key, value)\r\n keep.add(key)\r\n }\r\n }\r\n\r\n this.attributes!.set(\"class\", userClass ? `${autoClass} ${userClass}` : autoClass)\r\n\r\n if (this.attributes!.items) {\r\n for (const name of Object.keys(this.attributes!.items)) {\r\n if (!keep.has(name)) this.attributes!.remove(name)\r\n }\r\n }\r\n\r\n if (this._events) {\r\n for (const key in this._events) this._bindEvent(key as EventName)\r\n }\r\n }\r\n\r\n addEvent(name: EventName, callback: (event: Event, node: ElementNode) => void): void {\r\n\r\n this._events = this._events || {}\r\n\r\n let current = this._events[name]\r\n if (typeof current == \"function\") {\r\n this._events[name] = (event: Event, node: ElementNode) => {\r\n current!(event, node)\r\n callback(event, node)\r\n }\r\n } else {\r\n this._events[name] = callback\r\n }\r\n }\r\n\r\n addHook<K extends keyof HookMap>(name: K, callback: HookMap[K]): void {\r\n const current = this._hooks[name];\r\n\r\n if (typeof current === \"function\") {\r\n const composed = ((...args: any[]) => {\r\n (current as Function)(...args);\r\n (callback as Function)(...args);\r\n }) as HookMap[K];\r\n // Preserve the maximum declared arity across composed hooks. Removal logic\r\n // inspects BeforeRemove.length (>= 2 means the hook owns `done()`, e.g. an\r\n // exit animation); a naive (...args) wrapper would report 0 and break that.\r\n try {\r\n Object.defineProperty(composed, \"length\", {\r\n value: Math.max((current as Function).length, (callback as Function).length),\r\n configurable: true,\r\n });\r\n } catch { /* length non-configurable on some engines — best effort */ }\r\n this._hooks[name] = composed;\r\n } else {\r\n this._hooks[name] = callback;\r\n }\r\n }\r\n getRoot(): ElementNode {\r\n let root: ElementNode = this;\r\n while (root && root instanceof ElementNode && root.parent) {\r\n root = root.parent;\r\n }\r\n return root\r\n }\r\n\r\n getContext(name: string): any {\r\n let node: ElementNode | null = this;\r\n while (node && (!node._context || !Object.prototype.hasOwnProperty.call(node._context, name))) {\r\n node = node.parent;\r\n }\r\n return node && node._context ? node._context[name] : undefined;\r\n }\r\n\r\n setContext(name: string, value: any) {\r\n this._context = this._context || {}\r\n this._context[name] = value;\r\n }\r\n\r\n getMetadata(name: string): any {\r\n return this._metadata ? this._metadata[name] : undefined;\r\n }\r\n\r\n setMetadata(key: string, value: any) {\r\n this._metadata = this._metadata || {}\r\n this._metadata[key] = value;\r\n\r\n }\r\n\r\n generateCSS(): string {\r\n if (!this.styles || !this.children) return \"\";\r\n let css = this.styles.cssText()\r\n css += this.children.items.map(child => child instanceof ElementNode ? child.generateCSS() : \"\").join(\"\")\r\n return css\r\n }\r\n\r\n generateHTML(): string {\r\n if (!this.children || !this.attributes) return \"\";\r\n const attributes = this.attributes.generateHTML();\r\n // Void elements must not emit a closing tag — `<br></br>` is parsed by the\r\n // HTML tokenizer as two <br>, which corrupts hydration child alignment.\r\n if ((VoidTags as readonly string[]).includes(this.tagName)) {\r\n return `<${this.tagName}${attributes}>`;\r\n }\r\n const content = this.children.generateHTML();\r\n return `<${this.tagName}${attributes}>${content}</${this.tagName}>`;\r\n }\r\n\r\n mount(domElement: HTMLElement, domStyle?: HTMLStyleElement): void {\r\n if (!domElement) throw new Error(\"Missing dom node on bind\");\r\n this.domElement = domElement;\r\n\r\n if (this._events) {\r\n for (const key in this._events) this._bindEvent(key as EventName)\r\n }\r\n\r\n if (this.children) {\r\n this.children.items.forEach((child, i) => {\r\n const childNode = domElement.childNodes[i];\r\n if (!childNode) return;\r\n if (child instanceof ElementNode) {\r\n child.mount(childNode as HTMLElement);\r\n } else {\r\n // Bind the server-rendered text/inline-HTML node so that reactive\r\n // child updates after hydration can locate and replace it.\r\n child.domText = childNode;\r\n }\r\n });\r\n }\r\n\r\n // Attach reactive style declarations to the server-rendered stylesheet so\r\n // post-hydration updates mutate the existing CSSOM rules instead of being\r\n // silently dropped (StyleProperty._domUpdate needs a bound domRule). Done\r\n // once from the call that received the style element, walking the whole\r\n // subtree because per-node selectors are globally unique.\r\n if (domStyle) {\r\n const sheet = domStyle.sheet;\r\n if (sheet) this._hydrateStyles(collectCSSRules(sheet.cssRules, new Map()));\r\n }\r\n\r\n this._hooks.Mount && this._hooks.Mount(this)\r\n }\r\n\r\n _hydrateStyles(domRuleMap: Map<string, CSSRule>): void {\r\n this.styles?.hydrate(domRuleMap);\r\n if (this.children) {\r\n for (const child of this.children.items) {\r\n if (child instanceof ElementNode) child._hydrateStyles(domRuleMap);\r\n }\r\n }\r\n }\r\n\r\n render(domElement: HTMLElement | SVGElement | DocumentFragment): HTMLElement | SVGElement {\r\n const newNode = this._createDOMNode();\r\n domElement.appendChild(newNode)\r\n this._hooks.Mount && this._hooks.Mount(this)\r\n let domStyle = this.getRoot().styles.domStyle\r\n let root = domElement.getRootNode()\r\n const styleParent = root instanceof ShadowRoot ? root : document.head\r\n domStyle ||= ensureDomStyle(styleParent)\r\n this.styles.render(domStyle as HTMLStyleElement)\r\n this.children.items.forEach(child => {\r\n if (child instanceof ElementNode && child._portal) {\r\n let dom = child._portal!(this.getRoot())\r\n dom && child.render(dom)\r\n } else {\r\n child.render(newNode)\r\n }\r\n })\r\n return newNode;\r\n }\r\n\r\n remove() {\r\n if (this.parent) {\r\n this.parent.children.remove(this)\r\n } else {\r\n // Root removal must also run BeforeRemove/Remove (and release reactive\r\n // subscriptions across the whole tree via _dispose), honoring async done().\r\n const done = () => {\r\n this.domElement?.remove()\r\n this._dispose()\r\n }\r\n if (this._hooks.BeforeRemove && this.domElement) {\r\n let called = false\r\n const once = () => { if (!called) { called = true; done() } }\r\n this._beforeRemoveFired = true\r\n this._hooks.BeforeRemove(this, once)\r\n if ((this._hooks.BeforeRemove as Function).length < 2 && !called) once()\r\n } else {\r\n done()\r\n }\r\n }\r\n }\r\n}\r\n","import { Notifier } from \"./Notifier.js\"\r\nimport { activeCollector } from \"./Collector.js\"\r\n\r\ntype Listener = (...args: any[]) => void\r\n\r\nexport class RecordState<T extends Record<string, any> = Record<string, any>> {\r\n private _notifier = new Notifier()\r\n private _record: T\r\n readonly initialRecord: T\r\n\r\n constructor(record: T) {\r\n this.initialRecord = { ...record }\r\n this._record = { ...record }\r\n }\r\n\r\n get<K extends keyof T>(key: K, l?: Listener): T[K] {\r\n if (l) {\r\n this._notifier.addListener(key as string, l)\r\n } else {\r\n // Auto-tracking: with no explicit listener, subscribe the active\r\n // collector for THIS key so a running computed/effect re-runs only\r\n // when this specific key changes. With no collector active the read\r\n // is untracked — the original behavior is preserved exactly.\r\n const collector = activeCollector()\r\n if (collector) this._notifier.addListener(key as string, collector.handler)\r\n }\r\n return this._record[key]\r\n }\r\n\r\n set<K extends keyof T>(key: K, value: T[K]): void {\r\n this._record[key] = value\r\n this._notifier.notify(key as string)\r\n }\r\n\r\n addListener<K extends keyof T>(key: K, fn: Listener): () => void {\r\n return this._notifier.addListener(key as string, fn)\r\n }\r\n\r\n removeListener<K extends keyof T>(key: K, fn: Listener): void {\r\n this._notifier.removeListener(key as string, fn)\r\n }\r\n\r\n reset<K extends keyof T>(key: K): void {\r\n this.set(key, this.initialRecord[key])\r\n }\r\n\r\n _dispose(): void {\r\n this._notifier._dispose()\r\n }\r\n}\r\n","import type { ValueListener } from \"./State.js\"\r\nimport { Collector, activeCollector, runWithCollector, runUntracked } from \"./Collector.js\"\r\nimport { Notifier, runBatched } from \"./Notifier.js\"\r\n\r\n// Derived-reactivity layer built ON TOP of State/RecordState + Notifier. Nothing\r\n// here forks a parallel reactivity system: every dependency is tracked by\r\n// subscribing a Collector's handler through the same Notifier.addListener path a\r\n// plain `state.get(listener)` uses, and every downstream notification goes\r\n// through Notifier.notify (so `_chain` cycle detection still applies).\r\n\r\n// ----------------------------------------------------------------------------\r\n// Reaction scheduler\r\n// ----------------------------------------------------------------------------\r\n//\r\n// An effect/computed subscribes its Collector handler to EACH of its\r\n// dependencies' Notifiers. When several dependencies change in one tick (or in\r\n// one `batch`), each dependency Notifier flushes in its own microtask and would\r\n// invoke the handler once per dependency. To re-run a reaction at most ONCE per\r\n// burst, the handler does not run its work inline; it enqueues a deduplicated\r\n// job. A single microtask drains the queue, and jobs enqueued while draining\r\n// (e.g. a downstream computed reacting) are processed in the same drain — so a\r\n// `batch` of writes collapses into a single downstream flush.\r\n\r\n// Microtask scheduler with the same `queueMicrotask` fallback as Notifier, for\r\n// older embedded Chromium runtimes that predate it.\r\nconst scheduleMicrotask: (callback: () => void) => void =\r\n typeof queueMicrotask === \"function\"\r\n ? queueMicrotask\r\n : (callback) => {\r\n Promise.resolve().then(callback).catch((error) => {\r\n setTimeout(() => { throw error }, 0)\r\n })\r\n }\r\n\r\nconst REACTION_QUEUE: Set<() => void> = new Set()\r\nlet reactionDrainScheduled = false\r\n\r\nfunction scheduleReaction(job: () => void): void {\r\n REACTION_QUEUE.add(job)\r\n if (reactionDrainScheduled) return\r\n reactionDrainScheduled = true\r\n scheduleMicrotask(drainReactions)\r\n}\r\n\r\nfunction drainReactions(): void {\r\n reactionDrainScheduled = false\r\n // Drain in passes: a job may enqueue more jobs (a computed re-running pushes to\r\n // its downstream computeds). Process until the queue settles.\r\n while (REACTION_QUEUE.size > 0) {\r\n const jobs = [...REACTION_QUEUE]\r\n REACTION_QUEUE.clear()\r\n for (const job of jobs) job()\r\n }\r\n}\r\n\r\n// ----------------------------------------------------------------------------\r\n// Scopes\r\n// ----------------------------------------------------------------------------\r\n\r\n// Disposer registered to a scope. A computed/effect/listener created inside a\r\n// scope's `run` adds its teardown here so `stop()` can release the whole graph\r\n// of a removed subtree in one call.\r\ntype Disposer = () => void\r\n\r\n// Stack of active scopes. Nested scopes register into the innermost one, and a\r\n// child scope is itself registered into its parent so stopping the parent stops\r\n// the child too.\r\nconst SCOPE_STACK: EffectScope[] = []\r\n\r\nfunction activeScope(): EffectScope | null {\r\n return SCOPE_STACK.length ? SCOPE_STACK[SCOPE_STACK.length - 1] : null\r\n}\r\n\r\nfunction registerDisposer(dispose: Disposer): void {\r\n const scope = activeScope()\r\n if (scope) scope._add(dispose)\r\n}\r\n\r\nexport interface EffectScopeHandle {\r\n // Run `fn` with this scope active; anything reactive created inside is owned\r\n // by the scope. Returns whatever `fn` returns.\r\n run<T>(fn: () => T): T\r\n // Dispose everything created inside this scope (and inside nested scopes).\r\n stop(): void\r\n}\r\n\r\nclass EffectScope implements EffectScopeHandle {\r\n private _disposers: Set<Disposer> = new Set()\r\n private _stopped = false\r\n\r\n // Register a teardown owned by this scope. Called by effect/computed/listener\r\n // creation and by nested-scope creation.\r\n _add(dispose: Disposer): void {\r\n if (this._stopped) {\r\n // The scope is already stopped; tear the new resource down immediately so\r\n // a late creation cannot leak.\r\n dispose()\r\n return\r\n }\r\n this._disposers.add(dispose)\r\n }\r\n\r\n run<T>(fn: () => T): T {\r\n SCOPE_STACK.push(this)\r\n try {\r\n return fn()\r\n } finally {\r\n SCOPE_STACK.pop()\r\n }\r\n }\r\n\r\n stop(): void {\r\n if (this._stopped) return\r\n this._stopped = true\r\n for (const dispose of this._disposers) dispose()\r\n this._disposers.clear()\r\n }\r\n}\r\n\r\n// Create an effect scope. Used so a removed subtree can dispose its reactive\r\n// graph in one `stop()` call. Nested scopes are owned by the enclosing scope.\r\nexport function effectScope(): EffectScopeHandle {\r\n const scope = new EffectScope()\r\n registerDisposer(() => scope.stop())\r\n return scope\r\n}\r\n\r\n// ----------------------------------------------------------------------------\r\n// Effect\r\n// ----------------------------------------------------------------------------\r\n\r\n// Run `fn` immediately, auto-tracking every reactive read inside it, and re-run\r\n// it whenever any tracked dependency changes. Returns a `dispose()` that releases\r\n// all current subscriptions. Each run re-collects dependencies, so reads no\r\n// longer reached (e.g. behind a branch) are dropped.\r\nexport function effect(fn: () => void): () => void {\r\n let disposed = false\r\n // `running` guards against an effect whose `fn` writes a state it also reads,\r\n // which would otherwise re-enter `run` mid-run.\r\n let running = false\r\n\r\n // A dependency changed: schedule a single deduplicated re-run. The job is the\r\n // SAME function reference each time, so the reaction queue's Set collapses\r\n // notifications from multiple dependencies (and from a batch) into one re-run.\r\n const job = (): void => {\r\n if (disposed) return\r\n run()\r\n }\r\n const collector = new Collector(() => {\r\n if (disposed) return\r\n scheduleReaction(job)\r\n })\r\n\r\n const run = (): void => {\r\n if (disposed || running) return\r\n running = true\r\n // Drop the previous run's dependencies so only deps read on THIS run remain\r\n // subscribed (stale-dep collection).\r\n collector.reset()\r\n try {\r\n runWithCollector(collector, fn)\r\n } finally {\r\n running = false\r\n }\r\n }\r\n\r\n const dispose = (): void => {\r\n if (disposed) return\r\n disposed = true\r\n collector.reset()\r\n REACTION_QUEUE.delete(job)\r\n }\r\n\r\n registerDisposer(dispose)\r\n run() // initial run is synchronous + immediate\r\n return dispose\r\n}\r\n\r\n// ----------------------------------------------------------------------------\r\n// Computed\r\n// ----------------------------------------------------------------------------\r\n\r\n// Read-only, State-like derived value. Subscribe to it exactly like a State:\r\n// `c.get()` for the current value, `c.get(listener)` to be notified on change,\r\n// and inside an element `(l) => c.get(l)` to bind the DOM.\r\nexport interface Computed<T> {\r\n readonly _isState: true\r\n // The computed's own Notifier (downstream subscriptions live here). Exposed,\r\n // like State._notifier, so subscription/leak inspection works uniformly.\r\n readonly _notifier: Notifier\r\n get(listener?: ValueListener<T>): T\r\n}\r\n\r\n// Lazy + cached derived value. `fn` is evaluated on first read and the result is\r\n// cached; it re-evaluates ONLY after a tracked dependency changes (a dirty flag),\r\n// never on every read. When a dependency changes, the computed recomputes and, if\r\n// the new value differs by `===` from the cached one, notifies its own\r\n// downstream listeners; an identical value short-circuits (no downstream churn).\r\nexport function computed<T>(fn: () => T): Computed<T> {\r\n // The computed publishes its own changes through a private Notifier (the same\r\n // machinery State uses), so anything subscribing to the computed participates\r\n // in the normal flush + cycle detection.\r\n const EVENT = \"computed\"\r\n const notifier = new Notifier()\r\n\r\n let cachedValue: T = undefined as unknown as T\r\n let dirty = true\r\n let hasValue = false\r\n\r\n // A dependency changed: schedule a single deduplicated reaction. Marking dirty\r\n // is immediate (so a synchronous read after the change recomputes); the\r\n // observed-path recompute+notify is deferred to the drain so multiple changing\r\n // dependencies (and a batch) collapse into one recompute.\r\n const job = (): void => {\r\n if (!dirty) return\r\n // If nothing is observing this computed, stay lazy — the next read recomputes.\r\n // If there ARE downstream listeners, recompute now to apply the equality\r\n // short-circuit and push the new value through this computed's own Notifier.\r\n if (notifier.listenerCount(EVENT) > 0) recomputeAndNotify()\r\n }\r\n const collector = new Collector(() => {\r\n if (dirty) return\r\n dirty = true\r\n scheduleReaction(job)\r\n })\r\n\r\n const recompute = (): void => {\r\n collector.reset()\r\n cachedValue = runWithCollector(collector, fn)\r\n dirty = false\r\n hasValue = true\r\n }\r\n\r\n const recomputeAndNotify = (): void => {\r\n const previous = cachedValue\r\n const had = hasValue\r\n recompute()\r\n // Equality short-circuit: an unchanged value must not notify downstream.\r\n if (had && cachedValue === previous) return\r\n notifier.notify(EVENT, cachedValue)\r\n }\r\n\r\n const get = (listener?: ValueListener<T>): T => {\r\n if (listener) {\r\n notifier.addListener(EVENT, listener)\r\n } else {\r\n // Auto-tracking: reading a computed inside another computed/effect makes\r\n // the outer computation depend on this one. Reusing State's collector path\r\n // means a chain of computeds composes through one Notifier graph.\r\n const outer = activeCollector()\r\n if (outer) notifier.addListener(EVENT, outer.handler)\r\n }\r\n if (dirty) recompute()\r\n return cachedValue\r\n }\r\n\r\n const dispose = (): void => {\r\n collector.reset()\r\n REACTION_QUEUE.delete(job)\r\n notifier._dispose()\r\n }\r\n registerDisposer(dispose)\r\n\r\n return { _isState: true, _notifier: notifier, get } as Computed<T>\r\n}\r\n\r\n// ----------------------------------------------------------------------------\r\n// batch / untrack\r\n// ----------------------------------------------------------------------------\r\n\r\n// Run `fn`, coalescing all State/RecordState/computed writes inside into a SINGLE\r\n// downstream flush after `fn` returns. Composes with the existing microtask flush\r\n// without double-flushing (see Notifier.runBatched). Returns `fn`'s result.\r\nexport function batch<T>(fn: () => T): T {\r\n return runBatched(fn)\r\n}\r\n\r\n// Run `fn` and return its result WITHOUT registering any reads into the currently\r\n// active collector. Useful to read a state inside an effect/computed without\r\n// making it a dependency.\r\nexport function untrack<T>(fn: () => T): T {\r\n return runUntracked(fn)\r\n}\r\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\r\n\r\nexport type Severity = \"error\" | \"warning\" | \"info\";\r\n\r\nexport interface Diagnostic {\r\n /** Rule id, e.g. \"inline-typography\". */\r\n rule: string;\r\n severity: Severity;\r\n /** Human path to the offending node, e.g. \"div > ul > li\". */\r\n path: string;\r\n message: string;\r\n /** How to fix it. */\r\n hint?: string;\r\n}\r\n\r\nexport interface DiagnoseOptions {\r\n /**\r\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\r\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\r\n * Set false if your reactive functions have side effects.\r\n */\r\n runReactive?: boolean;\r\n}\r\n\r\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\r\nconst VOID = new Set<string>(VoidTags);\r\nconst RESERVED = new Set([\r\n \"$\",\r\n \"style\",\r\n \"_key\",\r\n \"_portal\",\r\n \"_context\",\r\n \"_metadata\",\r\n]);\r\n// Inline these and the theme stops owning type scale / rhythm — use patches.\r\nconst TYPOGRAPHY_STYLE = new Set([\r\n \"fontSize\",\r\n \"lineHeight\",\r\n \"fontWeight\",\r\n \"letterSpacing\",\r\n]);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\r\n}\r\n\r\nfunction findTag(element: Record<string, unknown>): string | undefined {\r\n for (const key in element) {\r\n if (TAGS.has(key)) return key;\r\n }\r\n return undefined;\r\n}\r\n\r\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\r\nexport function diagnose(\r\n root: unknown,\r\n options: DiagnoseOptions = {},\r\n): Diagnostic[] {\r\n const out: Diagnostic[] = [];\r\n walk(root, \"\", out, false, options.runReactive !== false);\r\n return out;\r\n}\r\n\r\nfunction walk(\r\n node: unknown,\r\n path: string,\r\n out: Diagnostic[],\r\n dynamic: boolean,\r\n runReactive: boolean,\r\n): void {\r\n if (typeof node === \"function\") {\r\n if (!runReactive) return;\r\n let result: unknown;\r\n try {\r\n result = (node as (listener: unknown) => unknown)(() => {});\r\n } catch {\r\n return; // reactive fn threw without a real runtime — skip\r\n }\r\n walk(result, path, out, true, runReactive);\r\n return;\r\n }\r\n\r\n if (Array.isArray(node)) {\r\n const elementItems = node.filter(\r\n (child) => isPlainObject(child) && findTag(child),\r\n ) as Record<string, unknown>[];\r\n\r\n if (dynamic) {\r\n if (\r\n elementItems.length > 1 &&\r\n elementItems.some((item) => item._key === undefined)\r\n ) {\r\n out.push({\r\n rule: \"missing-key\",\r\n severity: \"warning\",\r\n path: path || \"(list)\",\r\n message:\r\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\r\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\r\n });\r\n }\r\n\r\n // unstable-key (heuristic): in a dynamic list every `_key` equals its\r\n // sibling position (0, 1, 2, …). That is the runtime footprint of\r\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\r\n // defeats the point of keying because keys shift when the list reorders.\r\n if (\r\n elementItems.length > 1 &&\r\n elementItems.every((item, index) => item._key === index)\r\n ) {\r\n out.push({\r\n rule: \"unstable-key\",\r\n severity: \"warning\",\r\n path: path || \"(list)\",\r\n message:\r\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\r\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\r\n });\r\n }\r\n }\r\n\r\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\r\n // (the reconciler cannot tell them apart). Decidable on any sibling array,\r\n // static or dynamic.\r\n const seenKeys = new Map<string, number>();\r\n for (const item of elementItems) {\r\n const key = item._key;\r\n if (key === undefined || key === null) continue;\r\n const literalKey = `${typeof key}:${String(key)}`;\r\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\r\n }\r\n for (const [literalKey, count] of seenKeys) {\r\n if (count > 1) {\r\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\r\n out.push({\r\n rule: \"duplicate-key\",\r\n severity: \"error\",\r\n path: path || \"(list)\",\r\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\r\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\r\n });\r\n }\r\n }\r\n\r\n node.forEach((child, index) => {\r\n walk(child, `${path}[${index}]`, out, false, runReactive);\r\n });\r\n return;\r\n }\r\n\r\n if (!isPlainObject(node)) return;\r\n\r\n const element = node;\r\n const tag = findTag(element);\r\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\r\n\r\n if (!tag) {\r\n const contentKeys = Object.keys(element).filter(\r\n (key) =>\r\n !RESERVED.has(key) &&\r\n !key.startsWith(\"_on\") &&\r\n !key.startsWith(\"on\") &&\r\n !key.startsWith(\"data\") &&\r\n !key.startsWith(\"aria\"),\r\n );\r\n if (contentKeys.length === 1) {\r\n out.push({\r\n rule: \"unknown-tag\",\r\n severity: \"warning\",\r\n path: here,\r\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\r\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\r\n });\r\n }\r\n return;\r\n }\r\n\r\n const content = element[tag];\r\n\r\n if (VOID.has(tag) && content !== null && content !== undefined) {\r\n out.push({\r\n rule: \"void-content\",\r\n severity: \"error\",\r\n path: here,\r\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\r\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\r\n });\r\n }\r\n\r\n if (isPlainObject(element.style)) {\r\n const style = element.style;\r\n for (const prop in style) {\r\n if (TYPOGRAPHY_STYLE.has(prop) && typeof style[prop] !== \"function\") {\r\n out.push({\r\n rule: \"inline-typography\",\r\n severity: \"warning\",\r\n path: here,\r\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\r\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\r\n });\r\n }\r\n }\r\n }\r\n\r\n walk(content, here, out, false, runReactive);\r\n}\r\n\r\n/** Issue counts by severity, plus the grand total. */\r\nexport interface ValidationSummary {\r\n error: number;\r\n warning: number;\r\n info: number;\r\n total: number;\r\n}\r\n\r\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\r\nexport interface ValidationReport {\r\n /** True when there are no `error`-severity diagnostics. */\r\n ok: boolean;\r\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\r\n issues: Diagnostic[];\r\n summary: ValidationSummary;\r\n}\r\n\r\n/**\r\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\r\n * the issue list, and counts by severity). `ok` is false when any `error`\r\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\r\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\r\n */\r\nexport function validate(\r\n root: unknown,\r\n options: DiagnoseOptions = {},\r\n): ValidationReport {\r\n const issues = diagnose(root, options);\r\n const summary: ValidationSummary = {\r\n error: 0,\r\n warning: 0,\r\n info: 0,\r\n total: issues.length,\r\n };\r\n for (const issue of issues) summary[issue.severity] += 1;\r\n return { ok: summary.error === 0, issues, summary };\r\n}\r\n\r\n/** Formats diagnostics as a readable report (one line per issue). */\r\nexport function format(diagnostics: Diagnostic[]): string {\r\n if (diagnostics.length === 0) return \"✓ No issues found.\";\r\n const icon = (s: Severity) =>\r\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\r\n return diagnostics\r\n .map(\r\n (d) =>\r\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\r\n )\r\n .join(\"\\n\");\r\n}\r\n"],"mappings":"0bAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,YAAAE,ICAA,IAAAC,EAAA,GAAAC,EAAAD,EAAA,cAAAE,EAAA,WAAAC,EAAA,aAAAC,ICAO,IAAMC,EAAkB,CAC7B,UACA,aACA,gBACA,iBACA,SACA,WACA,YACA,mBACA,WACA,UACA,UACA,gBACA,gBACA,oBACA,SACA,cACA,QACA,aACA,SACA,YACA,cACA,cACA,aACA,cACA,SACA,mBACA,YACA,UACA,UACA,UACA,aACA,UACA,YACA,YACA,aACA,UACA,SACA,eACA,mBACA,cACA,cACA,eACA,eACA,cACA,aACA,cACA,YACA,UACA,UACA,SACA,YACA,aACA,eACA,UACA,WACA,WACA,cACA,4BACA,WACA,YACA,WACA,eACA,YACA,WACA,YACA,eACA,WACA,iBACA,YACA,UACA,eACA,cACA,aACA,gBACA,gBACA,gBACA,cACA,kBACA,iBACA,iBACA,gBACA,eACA,sBACA,uBACA,qBACA,sBACA,mBACA,kBACA,oBACA,mBACA,iBACA,uBACA,qBACA,oBACA,YACA,YACF,EAEaC,EAAeD,EAAgB,OAAO,CAACE,EAAKC,IAAO,CAC9D,IAAMC,EAAMD,EAAG,MAAM,CAAC,EAAE,YAAY,EACpC,OAAAD,EAAIE,CAAG,EAAID,EACJD,CACT,EAAG,CAAC,CAAiF,ECvGxEG,EAAW,CACtB,IACA,OACA,UACA,UACA,QACA,QACA,IACA,OACA,aACA,KACA,SACA,SACA,UACA,OACA,OACA,MACA,WACA,OACA,WACA,KACA,MACA,UACA,MACA,SACA,MACA,KACA,KACA,KACA,WACA,aACA,SACA,SACA,OACA,KACA,KACA,KACA,KACA,KACA,KACA,SACA,SACA,IACA,SACA,MACA,QACA,MACA,MACA,QACA,SACA,KACA,OACA,MACA,OACA,OACA,QACA,MACA,WACA,SACA,KACA,WACA,SACA,SACA,IACA,QACA,UACA,MACA,WACA,IACA,KACA,KACA,OACA,IACA,OACA,UACA,SACA,OACA,QACA,SACA,OACA,SACA,MACA,UACA,MACA,QACA,QACA,KACA,WACA,WACA,QACA,KACA,QACA,OACA,QACA,KACA,QACA,IACA,KACA,MACA,QACA,MACA,MACA,MACA,OACA,OACA,SACA,OACA,QACA,KACA,UACA,gBACA,mBACA,SACA,WACA,SACA,OACA,OACA,UACA,UACA,gBACA,sBACA,cACA,mBACA,oBACA,oBACA,iBACA,eACA,UACA,UACA,UACA,UACA,UACA,iBACA,UACA,UACA,cACA,eACA,WACA,eACA,qBACA,cACA,SACA,eACA,SACA,gBACA,IACA,QACA,OACA,iBACA,SACA,OACA,WACA,QACA,OACA,UACA,UACA,WACA,WACA,iBACA,OACA,MACA,aACA,OACA,MACA,SACA,SACA,SACA,OACA,WACA,QACA,MACA,MACF,EM5KO,IAAMC,EAAW,CACtB,OACA,OACA,KACA,MACA,QACA,KACA,MACA,QACA,OACA,OACA,SACA,QACA,KACF,ECdaC,EAAU,CAAC,MAAO,SAAU,OAAQ,OAAQ,UACrD,OAAQ,WAAY,UAAW,IAAK,OACpC,MAAO,SAAU,iBAAkB,iBACnC,OAAQ,WAAY,OAAQ,SAAU,OACtC,QAAS,WAAY,QAAS,UAAW,SACzC,UAAW,mBAAoB,gBAC/B,iBAAkB,cAAe,gBACjC,UAAW,cAAe,WAAY,UACtC,UAAW,eAAe,EcgB9B,IAAMC,EAAO,IAAI,IAAY,CAAC,GAAGC,EAAU,GAAGC,CAAO,CAAC,EAChDC,EAAO,IAAI,IAAYC,CAAQ,EAC/BC,EAAW,IAAI,IAAI,CACvB,IACA,QACA,OACA,UACA,WACA,WACF,CAAC,EAEKC,EAAmB,IAAI,IAAI,CAC/B,WACA,aACA,aACA,eACF,CAAC,EAED,SAASC,EAAcC,EAAkD,CACvE,OAAO,OAAOA,GAAU,UAAYA,IAAU,MAAQ,CAAC,MAAM,QAAQA,CAAK,CAC5E,CAEA,SAASC,EAAQC,EAAsD,CACrE,QAAWC,KAAOD,EAChB,GAAIV,EAAK,IAAIW,CAAG,EAAG,OAAOA,CAG9B,CAGO,SAASC,EACdC,EACAC,EAA2B,CAAC,EACd,CACd,IAAMC,EAAoB,CAAC,E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","names":["global_exports","__export","src_exports","src_exports","__export","diagnose","format","validate","EventProperties","eventNameMap","acc","ev","key","HtmlTags","VoidTags","SvgTags","TAGS","q","pe","VOID","me","RESERVED","TYPOGRAPHY_STYLE","isPlainObject","value","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","result","e","elementItems","child","item","index","seenKeys","literalKey","count","tag","here","contentKeys","content","style","prop","validate","issues","summary","issue","format","diagnostics","icon","s","d"]}
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
- "use strict";var y=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var v=Object.prototype.hasOwnProperty;var $=(t,e)=>{for(var n in e)y(t,n,{get:e[n],enumerable:!0})},S=(t,e,n,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of k(e))!v.call(t,s)&&s!==n&&y(t,s,{get:()=>e[s],enumerable:!(c=w(e,s))||c.enumerable});return t};var b=t=>S(y({},"__esModule",{value:!0}),t);var T={};$(T,{diagnose:()=>m,format:()=>d});module.exports=b(T);var a=require("@domphy/core"),D=new Set([...a.HtmlTags,...a.SvgTags]),A=new Set(a.VoidTags),_=new Set(["$","style","_key","_portal","_context","_metadata"]),x=new Set(["fontSize","lineHeight","fontWeight","letterSpacing"]);function h(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function p(t){for(let e in t)if(D.has(e))return e}function m(t,e={}){let n=[];return u(t,"",n,!1,e.runReactive!==!1),n}function u(t,e,n,c,s){if(typeof t=="function"){if(!s)return;let r;try{r=t(()=>{})}catch(i){return}u(r,e,n,!0,s);return}if(Array.isArray(t)){if(c){let r=t.filter(i=>h(i)&&p(i));r.length>1&&r.some(i=>i._key===void 0)&&n.push({rule:"missing-key",severity:"warning",path:e||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."})}t.forEach((r,i)=>u(r,`${e}[${i}]`,n,!1,s));return}if(!h(t))return;let g=t,o=p(g),f=o?e?`${e} > ${o}`:o:e||"(root)";if(!o){let r=Object.keys(g).filter(i=>!_.has(i)&&!i.startsWith("_on")&&!i.startsWith("on")&&!i.startsWith("data")&&!i.startsWith("aria"));r.length===1&&n.push({rule:"unknown-tag",severity:"warning",path:f,message:`"${r[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let l=g[o];if(A.has(o)&&l!==null&&l!==void 0&&n.push({rule:"void-content",severity:"error",path:f,message:`Void tag "${o}" must have null content (got ${Array.isArray(l)?"array":typeof l}).`,hint:`Write { ${o}: null, \u2026 } and put attributes as sibling keys.`}),h(g.style)){let r=g.style;for(let i in r)x.has(i)&&typeof r[i]!="function"&&n.push({rule:"inline-typography",severity:"warning",path:f,message:`Inline \`${i}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."})}u(l,f,n,!1,s)}function d(t){if(t.length===0)return"\u2713 No issues found.";let e=n=>n==="error"?"\u2717":n==="warning"?"\u26A0":"i";return t.map(n=>`${e(n.severity)} [${n.rule}] ${n.path}
2
- ${n.message}${n.hint?`
3
- \u2192 ${n.hint}`:""}`).join(`
4
- `)}0&&(module.exports={diagnose,format});
1
+ "use strict";var m=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var x=(t,n)=>{for(var e in n)m(t,e,{get:n[e],enumerable:!0})},V=(t,n,e,l)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of D(n))!_.call(t,o)&&o!==e&&m(t,o,{get:()=>n[o],enumerable:!(l=S(n,o))||l.enumerable});return t};var A=t=>V(m({},"__esModule",{value:!0}),t);var j={};x(j,{diagnose:()=>k,format:()=>$,validate:()=>v});module.exports=A(j);var c=require("@domphy/core"),O=new Set([...c.HtmlTags,...c.SvgTags]),R=new Set(c.VoidTags),T=new Set(["$","style","_key","_portal","_context","_metadata"]),W=new Set(["fontSize","lineHeight","fontWeight","letterSpacing"]);function d(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function b(t){for(let n in t)if(O.has(n))return n}function k(t,n={}){let e=[];return h(t,"",e,!1,n.runReactive!==!1),e}function h(t,n,e,l,o){var w;if(typeof t=="function"){if(!o)return;let i;try{i=t(()=>{})}catch(r){return}h(i,n,e,!0,o);return}if(Array.isArray(t)){let i=t.filter(s=>d(s)&&b(s));l&&(i.length>1&&i.some(s=>s._key===void 0)&&e.push({rule:"missing-key",severity:"warning",path:n||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."}),i.length>1&&i.every((s,a)=>s._key===a)&&e.push({rule:"unstable-key",severity:"warning",path:n||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let r=new Map;for(let s of i){let a=s._key;if(a==null)continue;let p=`${typeof a}:${String(a)}`;r.set(p,((w=r.get(p))!=null?w:0)+1)}for(let[s,a]of r)if(a>1){let p=s.slice(s.indexOf(":")+1);e.push({rule:"duplicate-key",severity:"error",path:n||"(list)",message:`Duplicate \`_key\` "${p}" among ${a} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}t.forEach((s,a)=>{h(s,`${n}[${a}]`,e,!1,o)});return}if(!d(t))return;let y=t,u=b(y),f=u?n?`${n} > ${u}`:u:n||"(root)";if(!u){let i=Object.keys(y).filter(r=>!T.has(r)&&!r.startsWith("_on")&&!r.startsWith("on")&&!r.startsWith("data")&&!r.startsWith("aria"));i.length===1&&e.push({rule:"unknown-tag",severity:"warning",path:f,message:`"${i[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let g=y[u];if(R.has(u)&&g!==null&&g!==void 0&&e.push({rule:"void-content",severity:"error",path:f,message:`Void tag "${u}" must have null content (got ${Array.isArray(g)?"array":typeof g}).`,hint:`Write { ${u}: null, \u2026 } and put attributes as sibling keys.`}),d(y.style)){let i=y.style;for(let r in i)W.has(r)&&typeof i[r]!="function"&&e.push({rule:"inline-typography",severity:"warning",path:f,message:`Inline \`${r}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."})}h(g,f,e,!1,o)}function v(t,n={}){let e=k(t,n),l={error:0,warning:0,info:0,total:e.length};for(let o of e)l[o.severity]+=1;return{ok:l.error===0,issues:e,summary:l}}function $(t){if(t.length===0)return"\u2713 No issues found.";let n=e=>e==="error"?"\u2717":e==="warning"?"\u26A0":"i";return t.map(e=>`${n(e.severity)} [${e.rule}] ${e.path}
2
+ ${e.message}${e.hint?`
3
+ \u2192 ${e.hint}`:""}`).join(`
4
+ `)}0&&(module.exports={diagnose,format,validate});
5
5
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/diagnose.ts"],"sourcesContent":["// @domphy/doctor — static analyzer for Domphy element trees. Catches\n// non-idiomatic patterns (inline typography, void-tag content, missing _key on\n// dynamic lists, unknown tags) so humans and AI agents get a feedback loop to\n// self-correct generated code.\n\nexport type { DiagnoseOptions, Diagnostic, Severity } from \"./diagnose.js\";\nexport { diagnose, format } from \"./diagnose.js\";\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /** Human path to the offending node, e.g. \"div > ul > li\". */\n path: string;\n message: string;\n /** How to fix it. */\n hint?: string;\n}\n\nexport interface DiagnoseOptions {\n /**\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\n * Set false if your reactive functions have side effects.\n */\n runReactive?: boolean;\n}\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n// Inline these and the theme stops owning type scale / rhythm — use patches.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\nexport function diagnose(\n root: unknown,\n options: DiagnoseOptions = {},\n): Diagnostic[] {\n const out: Diagnostic[] = [];\n walk(root, \"\", out, false, options.runReactive !== false);\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n runReactive: boolean,\n): void {\n if (typeof node === \"function\") {\n if (!runReactive) return;\n let result: unknown;\n try {\n result = (node as (listener: unknown) => unknown)(() => {});\n } catch {\n return; // reactive fn threw without a real runtime — skip\n }\n walk(result, path, out, true, runReactive);\n return;\n }\n\n if (Array.isArray(node)) {\n if (dynamic) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n if (\n elementItems.length > 1 &&\n elementItems.some((item) => item._key === undefined)\n ) {\n out.push({\n rule: \"missing-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\n });\n }\n }\n node.forEach((child, index) =>\n walk(child, `${path}[${index}]`, out, false, runReactive),\n );\n return;\n }\n\n if (!isPlainObject(node)) return;\n\n const element = node;\n const tag = findTag(element);\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n if (!tag) {\n const contentKeys = Object.keys(element).filter(\n (key) =>\n !RESERVED.has(key) &&\n !key.startsWith(\"_on\") &&\n !key.startsWith(\"on\") &&\n !key.startsWith(\"data\") &&\n !key.startsWith(\"aria\"),\n );\n if (contentKeys.length === 1) {\n out.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n path: here,\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\n });\n }\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n out.push({\n rule: \"void-content\",\n severity: \"error\",\n path: here,\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\n });\n }\n\n if (isPlainObject(element.style)) {\n const style = element.style;\n for (const prop in style) {\n if (TYPOGRAPHY_STYLE.has(prop) && typeof style[prop] !== \"function\") {\n out.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n path: here,\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\n });\n }\n }\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Formats diagnostics as a readable report (one line per issue). */\nexport function format(diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return \"✓ No issues found.\";\n const icon = (s: Severity) =>\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\n return diagnostics\n .map(\n (d) =>\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n"],"mappings":"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","names":["src_exports","__export","diagnose","format","__toCommonJS","import_core","TAGS","VOID","RESERVED","TYPOGRAPHY_STYLE","isPlainObject","value","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","result","e","elementItems","child","item","index","tag","here","contentKeys","content","style","prop","format","diagnostics","icon","s","d"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/diagnose.ts"],"sourcesContent":["// @domphy/doctor — static analyzer for Domphy element trees. Catches\r\n// non-idiomatic patterns (inline typography, void-tag content, missing/duplicate/\r\n// unstable _key on lists, unknown tags) so humans and AI agents get a feedback\r\n// loop to self-correct generated code. `validate()` is the aggregate entry point.\r\n\r\nexport type {\r\n DiagnoseOptions,\r\n Diagnostic,\r\n Severity,\r\n ValidationReport,\r\n ValidationSummary,\r\n} from \"./diagnose.js\";\r\nexport { diagnose, format, validate } from \"./diagnose.js\";\r\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\r\n\r\nexport type Severity = \"error\" | \"warning\" | \"info\";\r\n\r\nexport interface Diagnostic {\r\n /** Rule id, e.g. \"inline-typography\". */\r\n rule: string;\r\n severity: Severity;\r\n /** Human path to the offending node, e.g. \"div > ul > li\". */\r\n path: string;\r\n message: string;\r\n /** How to fix it. */\r\n hint?: string;\r\n}\r\n\r\nexport interface DiagnoseOptions {\r\n /**\r\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\r\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\r\n * Set false if your reactive functions have side effects.\r\n */\r\n runReactive?: boolean;\r\n}\r\n\r\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\r\nconst VOID = new Set<string>(VoidTags);\r\nconst RESERVED = new Set([\r\n \"$\",\r\n \"style\",\r\n \"_key\",\r\n \"_portal\",\r\n \"_context\",\r\n \"_metadata\",\r\n]);\r\n// Inline these and the theme stops owning type scale / rhythm — use patches.\r\nconst TYPOGRAPHY_STYLE = new Set([\r\n \"fontSize\",\r\n \"lineHeight\",\r\n \"fontWeight\",\r\n \"letterSpacing\",\r\n]);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\r\n}\r\n\r\nfunction findTag(element: Record<string, unknown>): string | undefined {\r\n for (const key in element) {\r\n if (TAGS.has(key)) return key;\r\n }\r\n return undefined;\r\n}\r\n\r\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\r\nexport function diagnose(\r\n root: unknown,\r\n options: DiagnoseOptions = {},\r\n): Diagnostic[] {\r\n const out: Diagnostic[] = [];\r\n walk(root, \"\", out, false, options.runReactive !== false);\r\n return out;\r\n}\r\n\r\nfunction walk(\r\n node: unknown,\r\n path: string,\r\n out: Diagnostic[],\r\n dynamic: boolean,\r\n runReactive: boolean,\r\n): void {\r\n if (typeof node === \"function\") {\r\n if (!runReactive) return;\r\n let result: unknown;\r\n try {\r\n result = (node as (listener: unknown) => unknown)(() => {});\r\n } catch {\r\n return; // reactive fn threw without a real runtime — skip\r\n }\r\n walk(result, path, out, true, runReactive);\r\n return;\r\n }\r\n\r\n if (Array.isArray(node)) {\r\n const elementItems = node.filter(\r\n (child) => isPlainObject(child) && findTag(child),\r\n ) as Record<string, unknown>[];\r\n\r\n if (dynamic) {\r\n if (\r\n elementItems.length > 1 &&\r\n elementItems.some((item) => item._key === undefined)\r\n ) {\r\n out.push({\r\n rule: \"missing-key\",\r\n severity: \"warning\",\r\n path: path || \"(list)\",\r\n message:\r\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\r\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\r\n });\r\n }\r\n\r\n // unstable-key (heuristic): in a dynamic list every `_key` equals its\r\n // sibling position (0, 1, 2, …). That is the runtime footprint of\r\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\r\n // defeats the point of keying because keys shift when the list reorders.\r\n if (\r\n elementItems.length > 1 &&\r\n elementItems.every((item, index) => item._key === index)\r\n ) {\r\n out.push({\r\n rule: \"unstable-key\",\r\n severity: \"warning\",\r\n path: path || \"(list)\",\r\n message:\r\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\r\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\r\n });\r\n }\r\n }\r\n\r\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\r\n // (the reconciler cannot tell them apart). Decidable on any sibling array,\r\n // static or dynamic.\r\n const seenKeys = new Map<string, number>();\r\n for (const item of elementItems) {\r\n const key = item._key;\r\n if (key === undefined || key === null) continue;\r\n const literalKey = `${typeof key}:${String(key)}`;\r\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\r\n }\r\n for (const [literalKey, count] of seenKeys) {\r\n if (count > 1) {\r\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\r\n out.push({\r\n rule: \"duplicate-key\",\r\n severity: \"error\",\r\n path: path || \"(list)\",\r\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\r\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\r\n });\r\n }\r\n }\r\n\r\n node.forEach((child, index) => {\r\n walk(child, `${path}[${index}]`, out, false, runReactive);\r\n });\r\n return;\r\n }\r\n\r\n if (!isPlainObject(node)) return;\r\n\r\n const element = node;\r\n const tag = findTag(element);\r\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\r\n\r\n if (!tag) {\r\n const contentKeys = Object.keys(element).filter(\r\n (key) =>\r\n !RESERVED.has(key) &&\r\n !key.startsWith(\"_on\") &&\r\n !key.startsWith(\"on\") &&\r\n !key.startsWith(\"data\") &&\r\n !key.startsWith(\"aria\"),\r\n );\r\n if (contentKeys.length === 1) {\r\n out.push({\r\n rule: \"unknown-tag\",\r\n severity: \"warning\",\r\n path: here,\r\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\r\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\r\n });\r\n }\r\n return;\r\n }\r\n\r\n const content = element[tag];\r\n\r\n if (VOID.has(tag) && content !== null && content !== undefined) {\r\n out.push({\r\n rule: \"void-content\",\r\n severity: \"error\",\r\n path: here,\r\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\r\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\r\n });\r\n }\r\n\r\n if (isPlainObject(element.style)) {\r\n const style = element.style;\r\n for (const prop in style) {\r\n if (TYPOGRAPHY_STYLE.has(prop) && typeof style[prop] !== \"function\") {\r\n out.push({\r\n rule: \"inline-typography\",\r\n severity: \"warning\",\r\n path: here,\r\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\r\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\r\n });\r\n }\r\n }\r\n }\r\n\r\n walk(content, here, out, false, runReactive);\r\n}\r\n\r\n/** Issue counts by severity, plus the grand total. */\r\nexport interface ValidationSummary {\r\n error: number;\r\n warning: number;\r\n info: number;\r\n total: number;\r\n}\r\n\r\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\r\nexport interface ValidationReport {\r\n /** True when there are no `error`-severity diagnostics. */\r\n ok: boolean;\r\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\r\n issues: Diagnostic[];\r\n summary: ValidationSummary;\r\n}\r\n\r\n/**\r\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\r\n * the issue list, and counts by severity). `ok` is false when any `error`\r\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\r\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\r\n */\r\nexport function validate(\r\n root: unknown,\r\n options: DiagnoseOptions = {},\r\n): ValidationReport {\r\n const issues = diagnose(root, options);\r\n const summary: ValidationSummary = {\r\n error: 0,\r\n warning: 0,\r\n info: 0,\r\n total: issues.length,\r\n };\r\n for (const issue of issues) summary[issue.severity] += 1;\r\n return { ok: summary.error === 0, issues, summary };\r\n}\r\n\r\n/** Formats diagnostics as a readable report (one line per issue). */\r\nexport function format(diagnostics: Diagnostic[]): string {\r\n if (diagnostics.length === 0) return \"✓ No issues found.\";\r\n const icon = (s: Severity) =>\r\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\r\n return diagnostics\r\n .map(\r\n (d) =>\r\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\r\n )\r\n .join(\"\\n\");\r\n}\r\n"],"mappings":"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","names":["src_exports","__export","diagnose","format","validate","__toCommonJS","import_core","TAGS","VOID","RESERVED","TYPOGRAPHY_STYLE","isPlainObject","value","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","result","e","elementItems","child","item","index","seenKeys","literalKey","count","tag","here","contentKeys","content","style","prop","validate","issues","summary","issue","format","diagnostics","icon","s","d"]}
package/dist/index.d.cts CHANGED
@@ -19,7 +19,29 @@ interface DiagnoseOptions {
19
19
  }
20
20
  /** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */
21
21
  declare function diagnose(root: unknown, options?: DiagnoseOptions): Diagnostic[];
22
+ /** Issue counts by severity, plus the grand total. */
23
+ interface ValidationSummary {
24
+ error: number;
25
+ warning: number;
26
+ info: number;
27
+ total: number;
28
+ }
29
+ /** Structured result of {@link validate}: pass/fail flag, issues, and counts. */
30
+ interface ValidationReport {
31
+ /** True when there are no `error`-severity diagnostics. */
32
+ ok: boolean;
33
+ /** Every diagnostic found, across all rules (alias of `diagnose` output). */
34
+ issues: Diagnostic[];
35
+ summary: ValidationSummary;
36
+ }
37
+ /**
38
+ * Runs every diagnose rule and returns a structured report (pass/fail flag,
39
+ * the issue list, and counts by severity). `ok` is false when any `error`
40
+ * diagnostic is present; warnings/info do not flip `ok`. Use this as the single
41
+ * programmatic entry point; `diagnose`/`format` remain available for raw access.
42
+ */
43
+ declare function validate(root: unknown, options?: DiagnoseOptions): ValidationReport;
22
44
  /** Formats diagnostics as a readable report (one line per issue). */
23
45
  declare function format(diagnostics: Diagnostic[]): string;
24
46
 
25
- export { type DiagnoseOptions, type Diagnostic, type Severity, diagnose, format };
47
+ export { type DiagnoseOptions, type Diagnostic, type Severity, type ValidationReport, type ValidationSummary, diagnose, format, validate };
package/dist/index.d.ts CHANGED
@@ -19,7 +19,29 @@ interface DiagnoseOptions {
19
19
  }
20
20
  /** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */
21
21
  declare function diagnose(root: unknown, options?: DiagnoseOptions): Diagnostic[];
22
+ /** Issue counts by severity, plus the grand total. */
23
+ interface ValidationSummary {
24
+ error: number;
25
+ warning: number;
26
+ info: number;
27
+ total: number;
28
+ }
29
+ /** Structured result of {@link validate}: pass/fail flag, issues, and counts. */
30
+ interface ValidationReport {
31
+ /** True when there are no `error`-severity diagnostics. */
32
+ ok: boolean;
33
+ /** Every diagnostic found, across all rules (alias of `diagnose` output). */
34
+ issues: Diagnostic[];
35
+ summary: ValidationSummary;
36
+ }
37
+ /**
38
+ * Runs every diagnose rule and returns a structured report (pass/fail flag,
39
+ * the issue list, and counts by severity). `ok` is false when any `error`
40
+ * diagnostic is present; warnings/info do not flip `ok`. Use this as the single
41
+ * programmatic entry point; `diagnose`/`format` remain available for raw access.
42
+ */
43
+ declare function validate(root: unknown, options?: DiagnoseOptions): ValidationReport;
22
44
  /** Formats diagnostics as a readable report (one line per issue). */
23
45
  declare function format(diagnostics: Diagnostic[]): string;
24
46
 
25
- export { type DiagnoseOptions, type Diagnostic, type Severity, diagnose, format };
47
+ export { type DiagnoseOptions, type Diagnostic, type Severity, type ValidationReport, type ValidationSummary, diagnose, format, validate };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import{HtmlTags as h,SvgTags as p,VoidTags as m}from"@domphy/core";var d=new Set([...h,...p]),w=new Set(m),k=new Set(["$","style","_key","_portal","_context","_metadata"]),v=new Set(["fontSize","lineHeight","fontWeight","letterSpacing"]);function f(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function u(n){for(let i in n)if(d.has(i))return i}function $(n,i={}){let t=[];return c(n,"",t,!1,i.runReactive!==!1),t}function c(n,i,t,y,g){if(typeof n=="function"){if(!g)return;let r;try{r=n(()=>{})}catch(e){return}c(r,i,t,!0,g);return}if(Array.isArray(n)){if(y){let r=n.filter(e=>f(e)&&u(e));r.length>1&&r.some(e=>e._key===void 0)&&t.push({rule:"missing-key",severity:"warning",path:i||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."})}n.forEach((r,e)=>c(r,`${i}[${e}]`,t,!1,g));return}if(!f(n))return;let o=n,s=u(o),l=s?i?`${i} > ${s}`:s:i||"(root)";if(!s){let r=Object.keys(o).filter(e=>!k.has(e)&&!e.startsWith("_on")&&!e.startsWith("on")&&!e.startsWith("data")&&!e.startsWith("aria"));r.length===1&&t.push({rule:"unknown-tag",severity:"warning",path:l,message:`"${r[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let a=o[s];if(w.has(s)&&a!==null&&a!==void 0&&t.push({rule:"void-content",severity:"error",path:l,message:`Void tag "${s}" must have null content (got ${Array.isArray(a)?"array":typeof a}).`,hint:`Write { ${s}: null, \u2026 } and put attributes as sibling keys.`}),f(o.style)){let r=o.style;for(let e in r)v.has(e)&&typeof r[e]!="function"&&t.push({rule:"inline-typography",severity:"warning",path:l,message:`Inline \`${e}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."})}c(a,l,t,!1,g)}function S(n){if(n.length===0)return"\u2713 No issues found.";let i=t=>t==="error"?"\u2717":t==="warning"?"\u26A0":"i";return n.map(t=>`${i(t.severity)} [${t.rule}] ${t.path}
2
- ${t.message}${t.hint?`
3
- \u2192 ${t.hint}`:""}`).join(`
4
- `)}export{$ as diagnose,S as format};
1
+ import{HtmlTags as w,SvgTags as b,VoidTags as v}from"@domphy/core";var $=new Set([...w,...b]),S=new Set(v),D=new Set(["$","style","_key","_portal","_context","_metadata"]),_=new Set(["fontSize","lineHeight","fontWeight","letterSpacing"]);function h(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function d(t){for(let n in t)if($.has(n))return n}function k(t,n={}){let e=[];return p(t,"",e,!1,n.runReactive!==!1),e}function p(t,n,e,u,l){var m;if(typeof t=="function"){if(!l)return;let i;try{i=t(()=>{})}catch(r){return}p(i,n,e,!0,l);return}if(Array.isArray(t)){let i=t.filter(s=>h(s)&&d(s));u&&(i.length>1&&i.some(s=>s._key===void 0)&&e.push({rule:"missing-key",severity:"warning",path:n||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."}),i.length>1&&i.every((s,o)=>s._key===o)&&e.push({rule:"unstable-key",severity:"warning",path:n||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let r=new Map;for(let s of i){let o=s._key;if(o==null)continue;let f=`${typeof o}:${String(o)}`;r.set(f,((m=r.get(f))!=null?m:0)+1)}for(let[s,o]of r)if(o>1){let f=s.slice(s.indexOf(":")+1);e.push({rule:"duplicate-key",severity:"error",path:n||"(list)",message:`Duplicate \`_key\` "${f}" among ${o} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}t.forEach((s,o)=>{p(s,`${n}[${o}]`,e,!1,l)});return}if(!h(t))return;let c=t,a=d(c),g=a?n?`${n} > ${a}`:a:n||"(root)";if(!a){let i=Object.keys(c).filter(r=>!D.has(r)&&!r.startsWith("_on")&&!r.startsWith("on")&&!r.startsWith("data")&&!r.startsWith("aria"));i.length===1&&e.push({rule:"unknown-tag",severity:"warning",path:g,message:`"${i[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let y=c[a];if(S.has(a)&&y!==null&&y!==void 0&&e.push({rule:"void-content",severity:"error",path:g,message:`Void tag "${a}" must have null content (got ${Array.isArray(y)?"array":typeof y}).`,hint:`Write { ${a}: null, \u2026 } and put attributes as sibling keys.`}),h(c.style)){let i=c.style;for(let r in i)_.has(r)&&typeof i[r]!="function"&&e.push({rule:"inline-typography",severity:"warning",path:g,message:`Inline \`${r}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."})}p(y,g,e,!1,l)}function x(t,n={}){let e=k(t,n),u={error:0,warning:0,info:0,total:e.length};for(let l of e)u[l.severity]+=1;return{ok:u.error===0,issues:e,summary:u}}function V(t){if(t.length===0)return"\u2713 No issues found.";let n=e=>e==="error"?"\u2717":e==="warning"?"\u26A0":"i";return t.map(e=>`${n(e.severity)} [${e.rule}] ${e.path}
2
+ ${e.message}${e.hint?`
3
+ \u2192 ${e.hint}`:""}`).join(`
4
+ `)}export{k as diagnose,V as format,x as validate};
5
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/diagnose.ts"],"sourcesContent":["import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /** Human path to the offending node, e.g. \"div > ul > li\". */\n path: string;\n message: string;\n /** How to fix it. */\n hint?: string;\n}\n\nexport interface DiagnoseOptions {\n /**\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\n * Set false if your reactive functions have side effects.\n */\n runReactive?: boolean;\n}\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n// Inline these and the theme stops owning type scale / rhythm — use patches.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\nexport function diagnose(\n root: unknown,\n options: DiagnoseOptions = {},\n): Diagnostic[] {\n const out: Diagnostic[] = [];\n walk(root, \"\", out, false, options.runReactive !== false);\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n runReactive: boolean,\n): void {\n if (typeof node === \"function\") {\n if (!runReactive) return;\n let result: unknown;\n try {\n result = (node as (listener: unknown) => unknown)(() => {});\n } catch {\n return; // reactive fn threw without a real runtime — skip\n }\n walk(result, path, out, true, runReactive);\n return;\n }\n\n if (Array.isArray(node)) {\n if (dynamic) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n if (\n elementItems.length > 1 &&\n elementItems.some((item) => item._key === undefined)\n ) {\n out.push({\n rule: \"missing-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\n });\n }\n }\n node.forEach((child, index) =>\n walk(child, `${path}[${index}]`, out, false, runReactive),\n );\n return;\n }\n\n if (!isPlainObject(node)) return;\n\n const element = node;\n const tag = findTag(element);\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n if (!tag) {\n const contentKeys = Object.keys(element).filter(\n (key) =>\n !RESERVED.has(key) &&\n !key.startsWith(\"_on\") &&\n !key.startsWith(\"on\") &&\n !key.startsWith(\"data\") &&\n !key.startsWith(\"aria\"),\n );\n if (contentKeys.length === 1) {\n out.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n path: here,\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\n });\n }\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n out.push({\n rule: \"void-content\",\n severity: \"error\",\n path: here,\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\n });\n }\n\n if (isPlainObject(element.style)) {\n const style = element.style;\n for (const prop in style) {\n if (TYPOGRAPHY_STYLE.has(prop) && typeof style[prop] !== \"function\") {\n out.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n path: here,\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\n });\n }\n }\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Formats diagnostics as a readable report (one line per issue). */\nexport function format(diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return \"✓ No issues found.\";\n const icon = (s: Severity) =>\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\n return diagnostics\n .map(\n (d) =>\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n"],"mappings":"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","names":["HtmlTags","SvgTags","VoidTags","TAGS","VOID","RESERVED","TYPOGRAPHY_STYLE","isPlainObject","value","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","result","elementItems","child","item","index","tag","here","contentKeys","content","style","prop","format","diagnostics","icon","s","d"]}
1
+ {"version":3,"sources":["../src/diagnose.ts"],"sourcesContent":["import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\r\n\r\nexport type Severity = \"error\" | \"warning\" | \"info\";\r\n\r\nexport interface Diagnostic {\r\n /** Rule id, e.g. \"inline-typography\". */\r\n rule: string;\r\n severity: Severity;\r\n /** Human path to the offending node, e.g. \"div > ul > li\". */\r\n path: string;\r\n message: string;\r\n /** How to fix it. */\r\n hint?: string;\r\n}\r\n\r\nexport interface DiagnoseOptions {\r\n /**\r\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\r\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\r\n * Set false if your reactive functions have side effects.\r\n */\r\n runReactive?: boolean;\r\n}\r\n\r\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\r\nconst VOID = new Set<string>(VoidTags);\r\nconst RESERVED = new Set([\r\n \"$\",\r\n \"style\",\r\n \"_key\",\r\n \"_portal\",\r\n \"_context\",\r\n \"_metadata\",\r\n]);\r\n// Inline these and the theme stops owning type scale / rhythm — use patches.\r\nconst TYPOGRAPHY_STYLE = new Set([\r\n \"fontSize\",\r\n \"lineHeight\",\r\n \"fontWeight\",\r\n \"letterSpacing\",\r\n]);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\r\n}\r\n\r\nfunction findTag(element: Record<string, unknown>): string | undefined {\r\n for (const key in element) {\r\n if (TAGS.has(key)) return key;\r\n }\r\n return undefined;\r\n}\r\n\r\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\r\nexport function diagnose(\r\n root: unknown,\r\n options: DiagnoseOptions = {},\r\n): Diagnostic[] {\r\n const out: Diagnostic[] = [];\r\n walk(root, \"\", out, false, options.runReactive !== false);\r\n return out;\r\n}\r\n\r\nfunction walk(\r\n node: unknown,\r\n path: string,\r\n out: Diagnostic[],\r\n dynamic: boolean,\r\n runReactive: boolean,\r\n): void {\r\n if (typeof node === \"function\") {\r\n if (!runReactive) return;\r\n let result: unknown;\r\n try {\r\n result = (node as (listener: unknown) => unknown)(() => {});\r\n } catch {\r\n return; // reactive fn threw without a real runtime — skip\r\n }\r\n walk(result, path, out, true, runReactive);\r\n return;\r\n }\r\n\r\n if (Array.isArray(node)) {\r\n const elementItems = node.filter(\r\n (child) => isPlainObject(child) && findTag(child),\r\n ) as Record<string, unknown>[];\r\n\r\n if (dynamic) {\r\n if (\r\n elementItems.length > 1 &&\r\n elementItems.some((item) => item._key === undefined)\r\n ) {\r\n out.push({\r\n rule: \"missing-key\",\r\n severity: \"warning\",\r\n path: path || \"(list)\",\r\n message:\r\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\r\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\r\n });\r\n }\r\n\r\n // unstable-key (heuristic): in a dynamic list every `_key` equals its\r\n // sibling position (0, 1, 2, …). That is the runtime footprint of\r\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\r\n // defeats the point of keying because keys shift when the list reorders.\r\n if (\r\n elementItems.length > 1 &&\r\n elementItems.every((item, index) => item._key === index)\r\n ) {\r\n out.push({\r\n rule: \"unstable-key\",\r\n severity: \"warning\",\r\n path: path || \"(list)\",\r\n message:\r\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\r\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\r\n });\r\n }\r\n }\r\n\r\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\r\n // (the reconciler cannot tell them apart). Decidable on any sibling array,\r\n // static or dynamic.\r\n const seenKeys = new Map<string, number>();\r\n for (const item of elementItems) {\r\n const key = item._key;\r\n if (key === undefined || key === null) continue;\r\n const literalKey = `${typeof key}:${String(key)}`;\r\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\r\n }\r\n for (const [literalKey, count] of seenKeys) {\r\n if (count > 1) {\r\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\r\n out.push({\r\n rule: \"duplicate-key\",\r\n severity: \"error\",\r\n path: path || \"(list)\",\r\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\r\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\r\n });\r\n }\r\n }\r\n\r\n node.forEach((child, index) => {\r\n walk(child, `${path}[${index}]`, out, false, runReactive);\r\n });\r\n return;\r\n }\r\n\r\n if (!isPlainObject(node)) return;\r\n\r\n const element = node;\r\n const tag = findTag(element);\r\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\r\n\r\n if (!tag) {\r\n const contentKeys = Object.keys(element).filter(\r\n (key) =>\r\n !RESERVED.has(key) &&\r\n !key.startsWith(\"_on\") &&\r\n !key.startsWith(\"on\") &&\r\n !key.startsWith(\"data\") &&\r\n !key.startsWith(\"aria\"),\r\n );\r\n if (contentKeys.length === 1) {\r\n out.push({\r\n rule: \"unknown-tag\",\r\n severity: \"warning\",\r\n path: here,\r\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\r\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\r\n });\r\n }\r\n return;\r\n }\r\n\r\n const content = element[tag];\r\n\r\n if (VOID.has(tag) && content !== null && content !== undefined) {\r\n out.push({\r\n rule: \"void-content\",\r\n severity: \"error\",\r\n path: here,\r\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\r\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\r\n });\r\n }\r\n\r\n if (isPlainObject(element.style)) {\r\n const style = element.style;\r\n for (const prop in style) {\r\n if (TYPOGRAPHY_STYLE.has(prop) && typeof style[prop] !== \"function\") {\r\n out.push({\r\n rule: \"inline-typography\",\r\n severity: \"warning\",\r\n path: here,\r\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\r\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\r\n });\r\n }\r\n }\r\n }\r\n\r\n walk(content, here, out, false, runReactive);\r\n}\r\n\r\n/** Issue counts by severity, plus the grand total. */\r\nexport interface ValidationSummary {\r\n error: number;\r\n warning: number;\r\n info: number;\r\n total: number;\r\n}\r\n\r\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\r\nexport interface ValidationReport {\r\n /** True when there are no `error`-severity diagnostics. */\r\n ok: boolean;\r\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\r\n issues: Diagnostic[];\r\n summary: ValidationSummary;\r\n}\r\n\r\n/**\r\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\r\n * the issue list, and counts by severity). `ok` is false when any `error`\r\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\r\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\r\n */\r\nexport function validate(\r\n root: unknown,\r\n options: DiagnoseOptions = {},\r\n): ValidationReport {\r\n const issues = diagnose(root, options);\r\n const summary: ValidationSummary = {\r\n error: 0,\r\n warning: 0,\r\n info: 0,\r\n total: issues.length,\r\n };\r\n for (const issue of issues) summary[issue.severity] += 1;\r\n return { ok: summary.error === 0, issues, summary };\r\n}\r\n\r\n/** Formats diagnostics as a readable report (one line per issue). */\r\nexport function format(diagnostics: Diagnostic[]): string {\r\n if (diagnostics.length === 0) return \"✓ No issues found.\";\r\n const icon = (s: Severity) =>\r\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\r\n return diagnostics\r\n .map(\r\n (d) =>\r\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\r\n )\r\n .join(\"\\n\");\r\n}\r\n"],"mappings":"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","names":["HtmlTags","SvgTags","VoidTags","TAGS","VOID","RESERVED","TYPOGRAPHY_STYLE","isPlainObject","value","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","result","e","elementItems","child","item","index","seenKeys","literalKey","count","tag","here","contentKeys","content","style","prop","validate","issues","summary","issue","format","diagnostics","icon","s","d"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domphy/doctor",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Domphy Doctor - static analyzer that flags non-idiomatic Domphy element trees (AI self-correction)",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -33,14 +33,14 @@
33
33
  "directory": "packages/doctor"
34
34
  },
35
35
  "peerDependencies": {
36
- "@domphy/core": "^0.12.0"
36
+ "@domphy/core": "^0.13.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^25.9.2",
40
40
  "tsup": "^8.5.0",
41
41
  "typescript": "^5.8.3",
42
42
  "vitest": "^4.0.18",
43
- "@domphy/core": "0.12.0"
43
+ "@domphy/core": "0.13.0"
44
44
  },
45
45
  "files": [
46
46
  "dist",