@domphy/doctor 0.9.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present Huu Khanh Nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @domphy/doctor
2
+
3
+ A static analyzer for Domphy element trees. It walks the plain-object tree and flags non-idiomatic patterns, giving humans — and especially **AI agents** — a feedback loop to self-correct generated code.
4
+
5
+ Because Domphy UIs are plain objects, the doctor can inspect them directly (no parser, no build step), including the output of reactive `(listener) => …` functions.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @domphy/doctor @domphy/core
11
+ ```
12
+
13
+ `@domphy/core` is a peer dependency (the doctor reads its tag tables).
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { diagnose, format } from "@domphy/doctor"
19
+
20
+ const App = {
21
+ div: [
22
+ { p: "Hello", style: { fontSize: "20px" } }, // inline typography
23
+ { input: "oops" }, // void tag with content
24
+ { dvi: "typo" }, // unknown tag
25
+ ],
26
+ }
27
+
28
+ const issues = diagnose(App)
29
+ console.log(format(issues))
30
+ // ⚠ [inline-typography] div > p
31
+ // Inline `fontSize` — avoid inline typography styles.
32
+ // → Use a typography patch (paragraph()/heading()/…) via $.
33
+ // ✗ [void-content] div > input
34
+ // Void tag "input" must have null content (got string).
35
+ // ⚠ [unknown-tag] div
36
+ // "dvi" is not a known HTML/SVG tag — likely a typo.
37
+ ```
38
+
39
+ `diagnose(element, options?)` returns `Diagnostic[]`:
40
+
41
+ ```ts
42
+ interface Diagnostic {
43
+ rule: string // "inline-typography" | "void-content" | "missing-key" | "unknown-tag"
44
+ severity: "error" | "warning" | "info"
45
+ path: string // "div > ul > li"
46
+ message: string
47
+ hint?: string
48
+ }
49
+ ```
50
+
51
+ ## Rules
52
+
53
+ | Rule | Severity | Catches |
54
+ | --- | --- | --- |
55
+ | `inline-typography` | warning | `fontSize`/`lineHeight`/`fontWeight`/`letterSpacing` literals in `style` — use a typography patch |
56
+ | `void-content` | error | a void tag (`input`, `img`, `br`, …) with non-null content |
57
+ | `missing-key` | warning | a **dynamic** list (from a reactive function) of element children missing `_key` |
58
+ | `unknown-tag` | warning | an element whose first key isn't a valid HTML/SVG tag (typo) |
59
+
60
+ By default the doctor **invokes reactive content functions** with a no-op listener to inspect their output (this is how `missing-key` is detected). Pass `{ runReactive: false }` if your reactive functions have side effects.
61
+
62
+ ## For AI agents
63
+
64
+ Run `diagnose()` on generated Domphy code and feed `format()` back to the model — it will fix the issues itself. This is the self-correction loop that lets agents write correct Domphy despite having little training data for it. See the repo `AGENTS.md` and [`llms.txt`](https://www.domphy.com/llms.txt) for the rules the doctor enforces.
@@ -0,0 +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}
2
+ ${e.message}${e.hint?`
3
+ \u2192 ${e.hint}`:""}`).join(`
4
+ `)}return R(L);})();
5
+ //# sourceMappingURL=doctor.global.js.map
@@ -0,0 +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"]}
package/dist/index.cjs ADDED
@@ -0,0 +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});
5
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,cAAAE,EAAA,WAAAC,IAAA,eAAAC,EAAAJ,GCAA,IAAAK,EAA4C,wBAwBtCC,EAAO,IAAI,IAAY,CAAC,GAAG,WAAU,GAAG,SAAO,CAAC,EAChDC,EAAO,IAAI,IAAY,UAAQ,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,GAAIP,EAAK,IAAIQ,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,GAVIvB,EAAK,IAAIuB,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":["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"]}
@@ -0,0 +1,25 @@
1
+ type Severity = "error" | "warning" | "info";
2
+ interface Diagnostic {
3
+ /** Rule id, e.g. "inline-typography". */
4
+ rule: string;
5
+ severity: Severity;
6
+ /** Human path to the offending node, e.g. "div > ul > li". */
7
+ path: string;
8
+ message: string;
9
+ /** How to fix it. */
10
+ hint?: string;
11
+ }
12
+ interface DiagnoseOptions {
13
+ /**
14
+ * Invoke reactive content functions `(listener) => …` with a no-op listener to
15
+ * analyze their output (catches missing `_key` in dynamic lists). Default true.
16
+ * Set false if your reactive functions have side effects.
17
+ */
18
+ runReactive?: boolean;
19
+ }
20
+ /** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */
21
+ declare function diagnose(root: unknown, options?: DiagnoseOptions): Diagnostic[];
22
+ /** Formats diagnostics as a readable report (one line per issue). */
23
+ declare function format(diagnostics: Diagnostic[]): string;
24
+
25
+ export { type DiagnoseOptions, type Diagnostic, type Severity, diagnose, format };
@@ -0,0 +1,25 @@
1
+ type Severity = "error" | "warning" | "info";
2
+ interface Diagnostic {
3
+ /** Rule id, e.g. "inline-typography". */
4
+ rule: string;
5
+ severity: Severity;
6
+ /** Human path to the offending node, e.g. "div > ul > li". */
7
+ path: string;
8
+ message: string;
9
+ /** How to fix it. */
10
+ hint?: string;
11
+ }
12
+ interface DiagnoseOptions {
13
+ /**
14
+ * Invoke reactive content functions `(listener) => …` with a no-op listener to
15
+ * analyze their output (catches missing `_key` in dynamic lists). Default true.
16
+ * Set false if your reactive functions have side effects.
17
+ */
18
+ runReactive?: boolean;
19
+ }
20
+ /** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */
21
+ declare function diagnose(root: unknown, options?: DiagnoseOptions): Diagnostic[];
22
+ /** Formats diagnostics as a readable report (one line per issue). */
23
+ declare function format(diagnostics: Diagnostic[]): string;
24
+
25
+ export { type DiagnoseOptions, type Diagnostic, type Severity, diagnose, format };
package/dist/index.js ADDED
@@ -0,0 +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};
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":"AAAA,OAAS,YAAAA,EAAU,WAAAC,EAAS,YAAAC,MAAgB,eAwB5C,IAAMC,EAAO,IAAI,IAAY,CAAC,GAAGH,EAAU,GAAGC,CAAO,CAAC,EAChDG,EAAO,IAAI,IAAYF,CAAQ,EAC/BG,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,GAAIP,EAAK,IAAIQ,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,OAAQ,GACN,MACF,CACAD,EAAKK,EAAQH,EAAMH,EAAK,GAAMK,CAAW,EACzC,MACF,CAEA,GAAI,MAAM,QAAQH,CAAI,EAAG,CACvB,GAAIE,EAAS,CACX,IAAMG,EAAeL,EAAK,OACvBM,GAAUhB,EAAcgB,CAAK,GAAKd,EAAQc,CAAK,CAClD,EAEED,EAAa,OAAS,GACtBA,EAAa,KAAME,GAASA,EAAK,OAAS,MAAS,GAEnDT,EAAI,KAAK,CACP,KAAM,cACN,SAAU,UACV,KAAMG,GAAQ,SACd,QACE,6GACF,KAAM,yEACR,CAAC,CAEL,CACAD,EAAK,QAAQ,CAACM,EAAOE,IACnBT,EAAKO,EAAO,GAAGL,CAAI,IAAIO,CAAK,IAAKV,EAAK,GAAOK,CAAW,CAC1D,EACA,MACF,CAEA,GAAI,CAACb,EAAcU,CAAI,EAAG,OAE1B,IAAMP,EAAUO,EACVS,EAAMjB,EAAQC,CAAO,EACrBiB,EAAOD,EAAOR,EAAO,GAAGA,CAAI,MAAMQ,CAAG,GAAKA,EAAOR,GAAQ,SAE/D,GAAI,CAACQ,EAAK,CACR,IAAME,EAAc,OAAO,KAAKlB,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,EACIiB,EAAY,SAAW,GACzBb,EAAI,KAAK,CACP,KAAM,cACN,SAAU,UACV,KAAMY,EACN,QAAS,IAAIC,EAAY,CAAC,CAAC,sDAC3B,KAAM,yEACR,CAAC,EAEH,MACF,CAEA,IAAMC,EAAUnB,EAAQgB,CAAG,EAY3B,GAVItB,EAAK,IAAIsB,CAAG,GAAKG,IAAY,MAAQA,IAAY,QACnDd,EAAI,KAAK,CACP,KAAM,eACN,SAAU,QACV,KAAMY,EACN,QAAS,aAAaD,CAAG,iCAAiC,MAAM,QAAQG,CAAO,EAAI,QAAU,OAAOA,CAAO,KAC3G,KAAM,WAAWH,CAAG,sDACtB,CAAC,EAGCnB,EAAcG,EAAQ,KAAK,EAAG,CAChC,IAAMoB,EAAQpB,EAAQ,MACtB,QAAWqB,KAAQD,EACbxB,EAAiB,IAAIyB,CAAI,GAAK,OAAOD,EAAMC,CAAI,GAAM,YACvDhB,EAAI,KAAK,CACP,KAAM,oBACN,SAAU,UACV,KAAMY,EACN,QAAS,YAAYI,CAAI,4CACzB,KAAM,gHACR,CAAC,CAGP,CAEAf,EAAKa,EAASF,EAAMZ,EAAK,GAAOK,CAAW,CAC7C,CAGO,SAASY,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":["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"]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@domphy/doctor",
3
+ "version": "0.9.0",
4
+ "description": "Domphy Doctor - static analyzer that flags non-idiomatic Domphy element trees (AI self-correction)",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "keywords": [
22
+ "domphy",
23
+ "doctor",
24
+ "lint",
25
+ "validate",
26
+ "ai"
27
+ ],
28
+ "author": "Huu Khanh Nguyen",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/domphy/domphy.git",
33
+ "directory": "packages/doctor"
34
+ },
35
+ "peerDependencies": {
36
+ "@domphy/core": "^0.9.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^25.9.2",
40
+ "tsup": "^8.5.0",
41
+ "typescript": "^5.8.3",
42
+ "vitest": "^4.0.18",
43
+ "@domphy/core": "0.9.0"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md"
48
+ ],
49
+ "unpkg": "dist/doctor.global.js",
50
+ "jsdelivr": "dist/doctor.global.js",
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "pack": "tsup && npm pack",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest"
57
+ }
58
+ }