@domphy/doctor 0.18.0 → 0.18.2
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/dist/doctor.global.js +4 -4
- package/dist/doctor.global.js.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -1
- package/dist/index.d.ts +69 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +16 -15
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"use strict";var D=Object.defineProperty;var V=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var F=(e,t)=>{for(var n in t)D(e,n,{get:t[n],enumerable:!0})},M=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of C(t))!E.call(e,a)&&a!==n&&D(e,a,{get:()=>t[a],enumerable:!(r=V(t,a))||r.enumerable});return e};var z=e=>M(D({},"__esModule",{value:!0}),e);var Z={};F(Z,{diagnose:()=>T,fix:()=>_,format:()=>N,validate:()=>S});module.exports=z(Z);var g=require("@domphy/palette");var p=require("@domphy/core"),P=new Set([...p.HtmlTags,...p.SvgTags]),w=new Set(p.VoidTags);function d(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function k(e){for(let t in e)if(P.has(t))return t}var B=new Set(["$","style","_key","_portal","_context","_metadata"]),H=new Set(["fontSize","lineHeight","fontWeight","letterSpacing","fontFamily","textDecoration"]),j=new Set(["color","backgroundColor","background","borderColor","border","outlineColor","outline","fill","stroke"]),G=/#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/,W=new Set(["margin","marginTop","marginRight","marginBottom","marginLeft","marginInline","marginBlock","marginInlineStart","marginInlineEnd","marginBlockStart","marginBlockEnd","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingInline","paddingBlock","paddingInlineStart","paddingInlineEnd","paddingBlockStart","paddingBlockEnd","gap","rowGap","columnGap"]),U=/^(\d+(?:\.\d+)?)(rem|em|px)$/;function x(e){let t=e.match(/^(increase|decrease|shift)-(\d+)$/);return t?{family:t[1],n:parseInt(t[2],10)}:null}function K(e){if(e==="inherit"||e==="base"||/^-?\d+$/.test(e))return!0;let t=x(e);return t?t.n<=17:!1}function Y(e){try{let t=e.trim(),n;if(t.startsWith("#")){let i=t;if(i.length===9&&(i=i.slice(0,7)),i.length===5&&(i=i.slice(0,4)),i.length===4&&(i=`#${i[1]}${i[1]}${i[2]}${i[2]}${i[3]}${i[3]}`),i.length!==7)return null;n=(0,g.hexToRgb)(i)}else if(/^rgba?\s*\(/.test(t))n=(0,g.cssRgbToRgb)(t);else return null;let r=(0,g.rgbToLab)(n),a=(0,g.labToLch)(r);return[a[0],a[1],a[2]]}catch(t){return null}}var q=/#[0-9a-fA-F]{3,8}\b|rgba?\s*\([^)]*\)/;function J(e){let t=q.exec(e);return t?t[0]:null}function Q(e){let[t,n,r]=e,a=Math.round((t-50)/10),i=Math.max(-9,Math.min(9,a)),u;Math.abs(i)<=1?u='"base"':i<0?u=`"decrease-${Math.abs(i)}"`:u=`"increase-${i}"`;let l;return n<12?l="neutral":r<30||r>=330?l="error":r<75?l="warning":r<165?l="success":(r<265,l="primary"),`(l) => themeColor(l, ${u}, "${l}") [perceptual LCH L=${Math.round(t)} C=${Math.round(n)} h=${Math.round(r)}\xB0]`}function X(e,t){let n=U.exec(t);if(!n)return null;let r=parseFloat(n[1]),a=n[2],i;return a==="rem"||a==="em"?i=Math.round(r*4):i=Math.round(r/4),i<=0?null:`${e}: themeSpacing(${i}) \u2014 themeSpacing(n)=n/4em, so ${i}/4=${i/4}em \u2248 ${t} at default density`}function T(e,t={}){let n=[];return v(e,"",n,!1,t.runReactive!==!1),n}function v(e,t,n,r,a){var O,L;if(typeof e=="function"){if(!a)return;let s;try{s=e(()=>{})}catch(c){return}v(s,t,n,!0,a);return}if(Array.isArray(e)){let s=e.filter(o=>d(o)&&k(o));r&&(s.length>1&&s.some(o=>o._key===void 0)&&n.push({rule:"missing-key",severity:"warning",path:t||"(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."}),s.length>1&&s.every((o,f)=>o._key===f)&&n.push({rule:"unstable-key",severity:"warning",path:t||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let c=new Map;for(let o of s){let f=o._key;if(f==null)continue;let h=`${typeof f}:${String(f)}`;c.set(h,((O=c.get(h))!=null?O:0)+1)}for(let[o,f]of c)if(f>1){let h=o.slice(o.indexOf(":")+1);n.push({rule:"duplicate-key",severity:"error",path:t||"(list)",message:`Duplicate \`_key\` "${h}" among ${f} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}e.forEach((o,f)=>{v(o,`${t}[${f}]`,n,!1,a)});return}if(!d(e))return;let i=e,u=k(i),l=u?t?`${t} > ${u}`:u:t||"(root)";if(!u){let s=Object.keys(i).filter(c=>!B.has(c)&&!c.startsWith("_on")&&!c.startsWith("on")&&!c.startsWith("data")&&!c.startsWith("aria"));s.length===1&&n.push({rule:"unknown-tag",severity:"warning",path:l,message:`"${s[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 m=i[u];if(w.has(u)&&m!==null&&m!==void 0&&n.push({rule:"void-content",severity:"error",path:l,message:`Void tag "${u}" must have null content (got ${Array.isArray(m)?"array":typeof m}).`,hint:`Write { ${u}: null, \u2026 } and put attributes as sibling keys.`}),d(i.style)){let s=i.style;for(let c in s){let o=s[c];if(H.has(c)&&typeof o!="function"&&n.push({rule:"inline-typography",severity:"warning",path:l,message:`Inline \`${c}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."}),j.has(c)&&typeof o=="string"&&G.test(o)){let f=(L=J(o))!=null?L:o,h=Y(f),I=h?Q(h):"(l) => themeColor(l, tone, colorName)";n.push({rule:"raw-theme-value",severity:"info",path:l,message:`Inline \`${c}\` uses a literal color (${o}).`,hint:`Prefer a theme token \u2014 ${I} \u2014 so theming and dark mode apply.`})}if(W.has(c)&&typeof o=="string"){let f=X(c,o);f&&n.push({rule:"raw-spacing-value",severity:"info",path:l,message:`Inline \`${c}: "${o}"\` uses a literal spacing value.`,hint:`Prefer themeSpacing() for theme density: ${f}`})}}}let y=i.dataTone;if(typeof y=="string")if(!K(y))n.push({rule:"unknown-tone",severity:"warning",path:l,message:`\`dataTone\` "${y}" is not a valid tone.`,hint:'Use "inherit", "base", a number, or "shift-N"/"increase-N"/"decrease-N" with N \u2264 17 (the ramp has 18 steps). Words like "surface"/"text" are not tones.'});else{let s=x(y);(s==null?void 0:s.family)==="shift"&&s.n>=4&&s.n<=13&&n.push({rule:"middle-surface-anchor",severity:"warning",path:l,message:`\`dataTone: "${y}"\` uses a mid-ramp surface anchor (steps 4\u201313). Child tones derived from this surface may clamp and collapse contrast.`,hint:"Prefer edge anchors: shift-0\u20133 for light surfaces, shift-14\u201317 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions."})}let b=i.dataDensity;if(typeof b=="string"&&b!=="inherit"){let s=x(b);!s||s.family==="shift"?n.push({rule:"unknown-density",severity:"warning",path:l,message:`\`dataDensity\` "${b}" is not a valid density offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20134. "shift-" is not valid for density.'}):s.n>4&&n.push({rule:"unknown-density",severity:"error",path:l,message:`\`dataDensity\` "${b}" N=${s.n} is out of range \u2014 the density scale has 5 steps (max offset: 4).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 4. Density factors: [0.75, 1, 1.5, 2, 2.5].'})}let $=i.dataSize;if(typeof $=="string"&&$!=="inherit"){let s=x($);!s||s.family==="shift"?n.push({rule:"unknown-size",severity:"warning",path:l,message:`\`dataSize\` "${$}" is not a valid size offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20137. "shift-" is not valid for size.'}):s.n>7&&n.push({rule:"unknown-size",severity:"error",path:l,message:`\`dataSize\` "${$}" N=${s.n} is out of range \u2014 the size scale has 8 steps (max offset: 7).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 7.'})}v(m,l,n,!1,a)}function S(e,t={}){let n=T(e,t),r={error:0,warning:0,info:0,total:n.length};for(let a of n)r[a.severity]+=1;return{ok:r.error===0,issues:n,summary:r}}function N(e){if(e.length===0)return"\u2713 No issues found.";let t=n=>n==="error"?"\u2717":n==="warning"?"\u26A0":"i";return e.map(n=>`${t(n.severity)} [${n.rule}] ${n.path}
|
|
1
|
+
"use strict";var O=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var Y=(e,t)=>{for(var n in t)O(e,n,{get:t[n],enumerable:!0})},q=(e,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of U(t))!K.call(e,o)&&o!==n&&O(e,o,{get:()=>t[o],enumerable:!(i=G(t,o))||i.enumerable});return e};var J=e=>q(O({},"__esModule",{value:!0}),e);var fe={};Y(fe,{diagnose:()=>L,fix:()=>j,format:()=>W,validate:()=>T});module.exports=J(fe);var p=require("@domphy/palette");var k=require("@domphy/core"),Q=new Set([...k.HtmlTags,...k.SvgTags]),R=new Set(k.VoidTags);function y(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function v(e){for(let t in e)if(Q.has(t))return t}var X=new Set(["$","style","_key","_portal","_context","_metadata","_doctorDisable"]),Z=new Set(["fontSize","lineHeight","fontWeight","letterSpacing","fontFamily","textDecoration"]),ee=new Set(["color","backgroundColor","background","borderColor","border","outlineColor","outline","fill","stroke"]),te=new Set(["color","fill","stroke","backgroundColor","outlineColor","borderColor","caretColor","accentColor","columnRuleColor","textDecorationColor"]),ne=new Set(["transparent","currentcolor","inherit","initial","unset","none","auto","revert","revert-layer",""]),B=/#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/,re=new Set(["margin","marginTop","marginRight","marginBottom","marginLeft","marginInline","marginBlock","marginInlineStart","marginInlineEnd","marginBlockStart","marginBlockEnd","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingInline","paddingBlock","paddingInlineStart","paddingInlineEnd","paddingBlockStart","paddingBlockEnd","gap","rowGap","columnGap"]),ie=/^(\d+(?:\.\d+)?)(rem|em|px)$/;function C(e){let t=e.match(/^(increase|decrease|shift)-(\d+)$/);return t?{family:t[1],n:parseInt(t[2],10)}:null}function se(e){if(e==="inherit"||e==="base"||/^-?\d+$/.test(e))return!0;let t=C(e);return t?t.n<=17:!1}function ae(e){try{let t=e.trim(),n;if(t.startsWith("#")){let s=t;if(s.length===9&&(s=s.slice(0,7)),s.length===5&&(s=s.slice(0,4)),s.length===4&&(s=`#${s[1]}${s[1]}${s[2]}${s[2]}${s[3]}${s[3]}`),s.length!==7)return null;n=(0,p.hexToRgb)(s)}else if(/^rgba?\s*\(/.test(t))n=(0,p.cssRgbToRgb)(t);else return null;let i=(0,p.rgbToLab)(n),o=(0,p.labToLch)(i);return[o[0],o[1],o[2]]}catch(t){return null}}var oe=/#[0-9a-fA-F]{3,8}\b|rgba?\s*\([^)]*\)/;function le(e){let t=oe.exec(e);return t?t[0]:null}function ce(e){let[t,n,i]=e,o=Math.round((t-50)/10),s=Math.max(-9,Math.min(9,o)),c;Math.abs(s)<=1?c='"base"':s<0?c=`"decrease-${Math.abs(s)}"`:c=`"increase-${s}"`;let g;return n<12?g="neutral":i<30||i>=330?g="error":i<75?g="warning":i<165?g="success":(i<265,g="primary"),`(l) => themeColor(l, ${c}, "${g}") [perceptual LCH L=${Math.round(t)} C=${Math.round(n)} h=${Math.round(i)}\xB0]`}function ue(e,t){let n=ie.exec(t);if(!n)return null;let i=parseFloat(n[1]),o=n[2],s;return o==="rem"||o==="em"?s=Math.round(i*4):s=Math.round(i/4),s<=0?null:`${e}: themeSpacing(${s}) \u2014 themeSpacing(n)=n/4em, so ${s}/4=${s/4}em \u2248 ${t} at default density`}function H(e,t,n,i,o){if(e===!0){for(let s of n)s.path!==i&&o.push(s);return}if(e!=null&&e!==!1){let s=new Set(Array.isArray(e)?e:[String(e)]);for(let c of t)s.has(c.rule)||o.push(c);for(let c of n)c.path===i&&s.has(c.rule)||o.push(c);return}o.push(...t),o.push(...n)}function L(e,t={}){let n=[];if(D(e,"",n,!1,t),t.only!==void 0){if(t.only.length===0)return[];let i=new Set(t.only);return n.filter(o=>i.has(o.rule))}if(t.exclude&&t.exclude.length>0){let i=new Set(t.exclude);return n.filter(o=>!i.has(o.rule))}return n}function D(e,t,n,i,o){var M,F,P;let s=o.runReactive!==!1;if(typeof e=="function"){if(!s)return;let r;try{r=e(()=>{})}catch(l){return}D(r,t,n,!0,o);return}if(Array.isArray(e)){let r=e.filter(a=>y(a)&&v(a));i&&(r.length>1&&r.some(a=>a._key===void 0)&&n.push({rule:"missing-key",severity:"warning",category:"key",path:t||"(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."}),r.length>1&&r.every((a,f)=>a._key===f)&&n.push({rule:"unstable-key",severity:"warning",category:"key",path:t||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let l=new Map;for(let a of r){let f=a._key;if(f==null)continue;let d=`${typeof f}:${String(f)}`;l.set(d,((M=l.get(d))!=null?M:0)+1)}for(let[a,f]of l)if(f>1){let d=a.slice(a.indexOf(":")+1);n.push({rule:"duplicate-key",severity:"error",category:"key",path:t||"(list)",message:`Duplicate \`_key\` "${d}" among ${f} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}e.forEach((a,f)=>{D(a,`${t}[${f}]`,n,!1,o)});return}if(!y(e))return;let c=e,g=v(c),u=g?t?`${t} > ${g}`:g:t||"(root)",h=[];if(!g){let r=Object.keys(c).filter(l=>!X.has(l)&&!l.startsWith("_on")&&!l.startsWith("on")&&!l.startsWith("data")&&!l.startsWith("aria"));r.length===1&&h.push({rule:"unknown-tag",severity:"warning",category:"structure",path:u,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)."}),H(c._doctorDisable,h,[],u,n);return}let b=c[g];if(R.has(g)&&b!==null&&b!==void 0&&h.push({rule:"void-content",severity:"error",category:"structure",path:u,message:`Void tag "${g}" must have null content (got ${Array.isArray(b)?"array":typeof b}).`,hint:`Write { ${g}: null, \u2026 } and put attributes as sibling keys.`}),y(c.style)){let r=c.style;for(let l in r){let a=r[l];if(Z.has(l)&&typeof a!="function"&&h.push({rule:"inline-typography",severity:"warning",category:"typography",path:u,message:`Inline \`${l}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."}),ee.has(l)&&typeof a=="string"&&B.test(a)){let f=(F=le(a))!=null?F:a,d=ae(f),S=d?ce(d):"(l) => themeColor(l, tone, colorName)";h.push({rule:"raw-theme-value",severity:"info",category:"theme",path:u,message:`Inline \`${l}\` uses a literal color (${a}).`,hint:`Prefer a theme token \u2014 ${S} \u2014 so theming and dark mode apply.`})}if(te.has(l)&&typeof a=="string"&&!B.test(a)&&!a.includes("(")&&!a.startsWith("--")&&!ne.has(a.trim().toLowerCase())&&h.push({rule:"raw-theme-value",severity:"info",category:"theme",path:u,message:`Inline \`${l}\` uses a CSS named color ("${a}").`,hint:`CSS named colors like "${a}" bypass theming and dark mode. Prefer (l) => themeColor(l, tone, colorName) \u2014 so the theme context applies.`}),re.has(l)&&typeof a=="string"){let f=ue(l,a);f&&h.push({rule:"raw-spacing-value",severity:"info",category:"theme",path:u,message:`Inline \`${l}: "${a}"\` uses a literal spacing value.`,hint:`Prefer themeSpacing() for theme density: ${f}`})}}}let V=y(c.style)?c.style.backgroundColor:void 0;if(typeof V=="function"&&s){let r;try{r=V(()=>{})}catch(l){}if(typeof r=="string"){let l=r.match(/var\(--[\w-]+-(\d+)\)$/);l&&parseInt(l[1],10)>0&&h.push({rule:"tone-background-inherit",severity:"warning",category:"theme",path:u,message:`\`style.backgroundColor\` uses a fixed tone (resolves to "${r}" at base context) instead of "inherit".`,hint:'backgroundColor should always be (l) => themeColor(l, "inherit"). To shift the surface tone, set dataTone on the container \u2014 it applies to all children uniformly.'})}}{let r=y(c.style)?c.style:null,l=r==null?void 0:r.color,a=r==null?void 0:r.backgroundColor;if(s&&typeof l=="function"&&typeof a=="function"){let f,d;try{f=l(()=>{}),d=a(()=>{})}catch(m){}let S=m=>{if(typeof m!="string")return null;let z=m.match(/var\(--[\w-]+-(\d+)\)$/);return z?parseInt(z[1],10):null},A=S(f),_=S(d);if(A!==null&&_!==null){let m=Math.abs(A-_);m<9&&h.push({rule:"low-contrast",severity:"warning",category:"theme",path:u,message:`Text/background shift gap is ${m} (shift-${A} vs shift-${_}) \u2014 contrast may be insufficient.`,hint:"Aim for \u22659 shift steps between text and surface. E.g. shift-0 bg + shift-9 text, or shift-11 text on a shift-0 surface. Increase the gap or rely on a parent dataTone to open it."})}}}let w=c.dataTone;if(typeof w=="string")if(!se(w))h.push({rule:"unknown-tone",severity:"warning",category:"data-attr",path:u,message:`\`dataTone\` "${w}" is not a valid tone.`,hint:'Use "inherit", "base", a number, or "shift-N"/"increase-N"/"decrease-N" with N \u2264 17 (the ramp has 18 steps). Words like "surface"/"text" are not tones.'});else{let r=C(w);(r==null?void 0:r.family)==="shift"&&r.n>=4&&r.n<=13&&h.push({rule:"middle-surface-anchor",severity:"warning",category:"data-attr",path:u,message:`\`dataTone: "${w}"\` uses a mid-ramp surface anchor (steps 4\u201313). Child tones derived from this surface may clamp and collapse contrast.`,hint:"Prefer edge anchors: shift-0\u20133 for light surfaces, shift-14\u201317 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions."})}let $=c.dataDensity;if(typeof $=="string"&&$!=="inherit"){let r=C($);!r||r.family==="shift"?h.push({rule:"unknown-density",severity:"warning",category:"data-attr",path:u,message:`\`dataDensity\` "${$}" is not a valid density offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20134. "shift-" is not valid for density.'}):r.n>4&&h.push({rule:"unknown-density",severity:"error",category:"data-attr",path:u,message:`\`dataDensity\` "${$}" N=${r.n} is out of range \u2014 the density scale has 5 steps (max offset: 4).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 4. Density factors: [0.75, 1, 1.5, 2, 2.5].'})}let x=c.dataSize;if(typeof x=="string"&&x!=="inherit"){let r=C(x);!r||r.family==="shift"?h.push({rule:"unknown-size",severity:"warning",category:"data-attr",path:u,message:`\`dataSize\` "${x}" is not a valid size offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20137. "shift-" is not valid for size.'}):r.n>7&&h.push({rule:"unknown-size",severity:"error",category:"data-attr",path:u,message:`\`dataSize\` "${x}" N=${r.n} is out of range \u2014 the size scale has 8 steps (max offset: 7).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 7.'})}if(o.rules&&o.rules.length>0)for(let r of o.rules){let l;try{l=r.check(c,u,g)}catch(a){continue}for(let a of l)h.push({rule:r.id,severity:(P=a.severity)!=null?P:r.severity,category:r.category,path:u,message:a.message,hint:a.hint})}let E=[];D(b,u,E,!1,o),H(c._doctorDisable,h,E,u,n)}function T(e,t={}){let n=L(e,t),i={error:0,warning:0,info:0,total:n.length};for(let o of n)i[o.severity]+=1;return{ok:i.error===0,issues:n,summary:i}}function W(e){if(e.length===0)return"\u2713 No issues found.";let t=n=>n==="error"?"\u2717":n==="warning"?"\u26A0":"i";return e.map(n=>`${t(n.severity)} [${n.rule}] ${n.path}
|
|
2
2
|
${n.message}${n.hint?`
|
|
3
3
|
\u2192 ${n.hint}`:""}`).join(`
|
|
4
|
-
`)}function
|
|
4
|
+
`)}function I(e){if(Array.isArray(e))return e.map(I);if(y(e)){let t={};for(let n in e)t[n]=I(e[n]);return t}return e}function j(e,t={}){let n=I(e),i=[];return N(n,"",i),{tree:n,applied:i,report:T(n,t)}}function N(e,t,n){if(Array.isArray(e)){for(let[s,c]of e.entries())N(c,`${t}[${s}]`,n);return}if(!y(e))return;let i=v(e);if(!i)return;let o=t?`${t} > ${i}`:i;R.has(i)&&e[i]!==null&&e[i]!==void 0&&(e[i]=null,n.push({rule:"void-content",path:o,message:`Void tag <${i}> cannot have content \u2014 cleared to null.`})),N(e[i],o,n)}0&&(module.exports={diagnose,fix,format,validate});
|
|
5
5
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/diagnose.ts","../src/shared.ts","../src/fix.ts"],"sourcesContent":["// @domphy/doctor — static analyzer for Domphy element trees. Catches\n// non-idiomatic patterns (inline typography, literal theme colors, unknown\n// tones, void-tag content, missing/duplicate/unstable _key on lists, unknown\n// tags) so humans and AI agents get a feedback loop to self-correct generated\n// code. `validate()` is the aggregate entry point.\n\nexport type {\n DiagnoseOptions,\n Diagnostic,\n Severity,\n ValidationReport,\n ValidationSummary,\n} from \"./diagnose.js\";\nexport { diagnose, format, validate } from \"./diagnose.js\";\nexport type { AppliedFix, FixResult } from \"./fix.js\";\nexport { fix } from \"./fix.js\";\n","import { cssRgbToRgb, hexToRgb, labToLch, rgbToLab } from \"@domphy/palette\";\nimport { findTag, isPlainObject, VOID } from \"./shared.js\";\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 RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n\n// Typography style properties that must not be set inline — use patches instead.\n// Expanded from bench data: fontFamily + textDecoration were missing and caused\n// agents to write { style: { fontFamily: \"...\" } } without correction.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n \"fontFamily\",\n \"textDecoration\",\n]);\n\n// Color-bearing style props that should resolve through a theme token rather\n// than a literal value, so theming and dark mode apply. Shorthands\n// (background/border/outline) are included because they often carry a color.\nconst COLOR_STYLE = new Set([\n \"color\",\n \"backgroundColor\",\n \"background\",\n \"borderColor\",\n \"border\",\n \"outlineColor\",\n \"outline\",\n \"fill\",\n \"stroke\",\n]);\n\n// A literal color value: hex (#rgb … #rrggbbaa) or an rgb()/rgba()/hsl()/hsla()\n// function. Keywords like transparent/currentColor/inherit are intentionally\n// allowed — they carry no theme meaning.\nconst LITERAL_COLOR = /#[0-9a-fA-F]{3,8}\\b|\\b(?:rgba?|hsla?)\\s*\\(/;\n\n// Spacing style properties where literal rem/em/px values should use themeSpacing().\n// These are layout, not typography, but themeSpacing() ensures density consistency.\n// Logical properties (paddingBlock, paddingInline, etc.) are included — they are\n// used in Domphy patches and must also go through themeSpacing() for density scaling.\nconst SPACING_STYLE = new Set([\n \"margin\",\n \"marginTop\",\n \"marginRight\",\n \"marginBottom\",\n \"marginLeft\",\n \"marginInline\",\n \"marginBlock\",\n \"marginInlineStart\",\n \"marginInlineEnd\",\n \"marginBlockStart\",\n \"marginBlockEnd\",\n \"padding\",\n \"paddingTop\",\n \"paddingRight\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"paddingInline\",\n \"paddingBlock\",\n \"paddingInlineStart\",\n \"paddingInlineEnd\",\n \"paddingBlockStart\",\n \"paddingBlockEnd\",\n \"gap\",\n \"rowGap\",\n \"columnGap\",\n]);\n\n// Matches literal spacing values like \"16px\", \"1.5rem\", \"2em\" but not \"auto\",\n// \"inherit\", \"0\" (unitless zero is fine), or computed values.\nconst LITERAL_SPACING = /^(\\d+(?:\\.\\d+)?)(rem|em|px)$/;\n\n// Parses \"increase-N\" / \"decrease-N\" / \"shift-N\" into family + numeric offset.\n// Returns null when the pattern doesn't match (grammar error).\nfunction parseOffset(\n value: string,\n): { family: \"increase\" | \"decrease\" | \"shift\"; n: number } | null {\n const m = value.match(/^(increase|decrease|shift)-(\\d+)$/);\n if (!m) return null;\n return {\n family: m[1] as \"increase\" | \"decrease\" | \"shift\",\n n: parseInt(m[2], 10),\n };\n}\n\n// Valid `dataTone` grammar AND range:\n// \"inherit\", \"base\", a bare integer, or shift-N/increase-N/decrease-N where N ≤ 17.\n// The default Domphy theme has 18 tone steps (0–17). Values with valid grammar\n// but N > 17 are also rejected here so they surface as `unknown-tone` errors.\nfunction isValidTone(value: string): boolean {\n if (value === \"inherit\" || value === \"base\") return true;\n if (/^-?\\d+$/.test(value)) return true;\n const parsed = parseOffset(value);\n if (!parsed) return false;\n return parsed.n <= 17; // tone ramp has 18 steps: 0–17\n}\n\n// ─── Chromametry integration ─────────────────────────────────────────────────\n\n/**\n * Parses a CSS color literal (hex or rgb/rgba) into LCH [L, C, h].\n * Returns null if parsing fails or the format is unsupported (named colors, hsl).\n * Uses @domphy/palette math (CIELAB via D65 reference white).\n */\nfunction parseLiteralToLch(value: string): [number, number, number] | null {\n try {\n const trimmed = value.trim();\n let rgb: number[];\n\n if (trimmed.startsWith(\"#\")) {\n let hex = trimmed;\n if (hex.length === 9) hex = hex.slice(0, 7); // strip alpha #rrggbbaa → #rrggbb\n if (hex.length === 5) hex = hex.slice(0, 4); // strip alpha #rgba → #rgb\n if (hex.length === 4) {\n hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;\n }\n if (hex.length !== 7) return null;\n rgb = hexToRgb(hex);\n } else if (/^rgba?\\s*\\(/.test(trimmed)) {\n rgb = cssRgbToRgb(trimmed);\n } else {\n return null; // hsl, named colors, custom-properties — skip\n }\n\n const lab = rgbToLab(rgb);\n const lch = labToLch(lab);\n return [lch[0], lch[1], lch[2]];\n } catch {\n return null;\n }\n}\n\n// Pulls the first parseable color token out of a (possibly shorthand) value.\n// Shorthands such as \"1px solid #ccc\" or \"0 0 4px rgba(0,0,0,.5)\" embed the\n// color among other tokens, so `parseLiteralToLch` (which expects the value to\n// START with the color) would otherwise miss it. Matches a #hex literal or a\n// complete rgb()/rgba() call (with its arguments). hsl()/named colors are left\n// for the generic fallback, matching parseLiteralToLch's own coverage.\nconst EMBEDDED_COLOR = /#[0-9a-fA-F]{3,8}\\b|rgba?\\s*\\([^)]*\\)/;\n\nfunction extractColorLiteral(value: string): string | null {\n const match = EMBEDDED_COLOR.exec(value);\n return match ? match[0] : null;\n}\n\n/**\n * Converts LCH coordinates into a concrete `themeColor()` call suggestion plus\n * a perceptual description. The tone and color-family are approximations for the\n * default Domphy theme (light, 10 neutral tones, base at mid-lightness).\n */\nfunction buildColorHint(lch: [number, number, number]): string {\n const [L, C, h] = lch;\n\n // Map lightness to a Domphy tone relative to base (~L50).\n // Each step ≈ 10 lightness units — clamp to ±9 (max offset in a 10-step ramp).\n const rawOffset = Math.round((L - 50) / 10);\n const offset = Math.max(-9, Math.min(9, rawOffset));\n let toneStr: string;\n if (Math.abs(offset) <= 1) toneStr = '\"base\"';\n else if (offset < 0) toneStr = `\"decrease-${Math.abs(offset)}\"`;\n else toneStr = `\"increase-${offset}\"`;\n\n // Infer the most likely semantic color family from chroma + hue.\n let colorFamily: string;\n if (C < 12) colorFamily = \"neutral\";\n else if (h < 30 || h >= 330)\n colorFamily = \"error\"; // red spectrum\n else if (h < 75)\n colorFamily = \"warning\"; // orange-yellow\n else if (h < 165)\n colorFamily = \"success\"; // green\n else if (h < 265)\n colorFamily = \"primary\"; // blue-indigo\n else colorFamily = \"primary\"; // violet → treat as primary\n\n return (\n `(l) => themeColor(l, ${toneStr}, \"${colorFamily}\") ` +\n `[perceptual LCH L=${Math.round(L)} C=${Math.round(C)} h=${Math.round(h)}°]`\n );\n}\n\n/**\n * Converts a literal spacing value like \"16px\" / \"1.5rem\" / \"2em\" into a\n * themeSpacing(n) suggestion. themeSpacing(n) = n/4 em, so n=4 → 1em ≈ 16px.\n */\nfunction buildSpacingHint(prop: string, value: string): string | null {\n const match = LITERAL_SPACING.exec(value);\n if (!match) return null;\n const amount = parseFloat(match[1]);\n const unit = match[2];\n let n: number;\n if (unit === \"rem\" || unit === \"em\") {\n n = Math.round(amount * 4);\n } else {\n // px: assume default 16px/rem → 1em = 16px\n n = Math.round(amount / 4);\n }\n if (n <= 0) return null;\n return `${prop}: themeSpacing(${n}) — themeSpacing(n)=n/4em, so ${n}/4=${n / 4}em ≈ ${value} at default density`;\n}\n\n// ─── Tree walkers ─────────────────────────────────────────────────────────────\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 const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n\n if (dynamic) {\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 // unstable-key (heuristic): in a dynamic list every `_key` equals its\n // sibling position (0, 1, 2, …). That is the runtime footprint of\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\n // defeats the point of keying because keys shift when the list reorders.\n if (\n elementItems.length > 1 &&\n elementItems.every((item, index) => item._key === index)\n ) {\n out.push({\n rule: \"unstable-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\n });\n }\n }\n\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\n const seenKeys = new Map<string, number>();\n for (const item of elementItems) {\n const key = item._key;\n if (key === undefined || key === null) continue;\n const literalKey = `${typeof key}:${String(key)}`;\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\n }\n for (const [literalKey, count] of seenKeys) {\n if (count > 1) {\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\n out.push({\n rule: \"duplicate-key\",\n severity: \"error\",\n path: path || \"(list)\",\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\n });\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 const value = style[prop];\n\n // inline-typography: typography properties must come from patches, not\n // inline style. fontFamily and textDecoration were missing from the original\n // set and are added here based on bench data showing persistent violations.\n if (TYPOGRAPHY_STYLE.has(prop) && typeof value !== \"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 // raw-theme-value: literal color values bypass theming/dark mode.\n // Enhanced with @domphy/palette chromametry: converts the literal color to\n // LCH and suggests the nearest themeColor() call with perceptual coordinates.\n if (\n COLOR_STYLE.has(prop) &&\n typeof value === \"string\" &&\n LITERAL_COLOR.test(value)\n ) {\n // For shorthands (border/outline/background) the color is embedded in a\n // larger string, so extract the color token first; fall back to the\n // whole value (covers the simple `color: \"#fff\"` case).\n const colorLiteral = extractColorLiteral(value) ?? value;\n const lch = parseLiteralToLch(colorLiteral);\n const colorHint = lch\n ? buildColorHint(lch)\n : \"(l) => themeColor(l, tone, colorName)\";\n\n out.push({\n rule: \"raw-theme-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}\\` uses a literal color (${value}).`,\n hint: `Prefer a theme token — ${colorHint} — so theming and dark mode apply.`,\n });\n }\n\n // raw-spacing-value: literal rem/em/px spacing values should use themeSpacing()\n // to respect the theme's density system. info-severity (soft recommendation).\n if (SPACING_STYLE.has(prop) && typeof value === \"string\") {\n const spacingHint = buildSpacingHint(prop, value);\n if (spacingHint) {\n out.push({\n rule: \"raw-spacing-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}: \"${value}\"\\` uses a literal spacing value.`,\n hint: `Prefer themeSpacing() for theme density: ${spacingHint}`,\n });\n }\n }\n }\n }\n\n // unknown-tone: dataTone is not valid grammar, or it's valid grammar but the\n // numeric offset is out of the 18-step ramp range (0–17).\n const dataTone = element.dataTone;\n if (typeof dataTone === \"string\") {\n if (!isValidTone(dataTone)) {\n out.push({\n rule: \"unknown-tone\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone\\` \"${dataTone}\" is not a valid tone.`,\n hint: 'Use \"inherit\", \"base\", a number, or \"shift-N\"/\"increase-N\"/\"decrease-N\" with N ≤ 17 (the ramp has 18 steps). Words like \"surface\"/\"text\" are not tones.',\n });\n } else {\n // middle-surface-anchor: shift-4 through shift-13 sets a mid-ramp surface\n // anchor. Children's tones may clamp and fold back, collapsing the contrast\n // between background and text. Edge anchors (0–3 light, 14–17 dark) are safe.\n const parsed = parseOffset(dataTone);\n if (parsed?.family === \"shift\" && parsed.n >= 4 && parsed.n <= 13) {\n out.push({\n rule: \"middle-surface-anchor\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone: \"${dataTone}\"\\` uses a mid-ramp surface anchor (steps 4–13). Child tones derived from this surface may clamp and collapse contrast.`,\n hint: \"Prefer edge anchors: shift-0–3 for light surfaces, shift-14–17 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions.\",\n });\n }\n }\n }\n\n // unknown-density: dataDensity value is invalid grammar or out of the 5-step\n // density range (increase/decrease 0–4; the scale factors are 0.75, 1, 1.5, 2, 2.5).\n const dataDensity = element.dataDensity;\n if (typeof dataDensity === \"string\" && dataDensity !== \"inherit\") {\n const parsed = parseOffset(dataDensity);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-density\",\n severity: \"warning\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" is not a valid density offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–4. \"shift-\" is not valid for density.',\n });\n } else if (parsed.n > 4) {\n out.push({\n rule: \"unknown-density\",\n severity: \"error\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" N=${parsed.n} is out of range — the density scale has 5 steps (max offset: 4).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 4. Density factors: [0.75, 1, 1.5, 2, 2.5].',\n });\n }\n }\n\n // unknown-size: dataSize value is invalid grammar or out of the 8-step size\n // range (increase/decrease 0–7).\n const dataSize = element.dataSize;\n if (typeof dataSize === \"string\" && dataSize !== \"inherit\") {\n const parsed = parseOffset(dataSize);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-size\",\n severity: \"warning\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" is not a valid size offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–7. \"shift-\" is not valid for size.',\n });\n } else if (parsed.n > 7) {\n out.push({\n rule: \"unknown-size\",\n severity: \"error\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" N=${parsed.n} is out of range — the size scale has 8 steps (max offset: 7).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 7.',\n });\n }\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Issue counts by severity, plus the grand total. */\nexport interface ValidationSummary {\n error: number;\n warning: number;\n info: number;\n total: number;\n}\n\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\nexport interface ValidationReport {\n /** True when there are no `error`-severity diagnostics. */\n ok: boolean;\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\n issues: Diagnostic[];\n summary: ValidationSummary;\n}\n\n/**\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\n * the issue list, and counts by severity). `ok` is false when any `error`\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\n */\nexport function validate(\n root: unknown,\n options: DiagnoseOptions = {},\n): ValidationReport {\n const issues = diagnose(root, options);\n const summary: ValidationSummary = {\n error: 0,\n warning: 0,\n info: 0,\n total: issues.length,\n };\n for (const issue of issues) summary[issue.severity] += 1;\n return { ok: summary.error === 0, issues, summary };\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","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\n\n// Internal helpers shared by diagnose.ts and fix.ts. Kept in one module so the\n// tag tables and the tree-shape predicates have a single source of truth.\n\n/** Every valid HTML and SVG tag name. */\nexport const TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\n\n/** Tags that render no children (input, img, br, …). */\nexport const VOID = new Set<string>(VoidTags);\n\n/** True for a non-array object (a Domphy element or a plain record). */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Returns the element's tag key (the first key that names a valid tag). */\nexport function 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","import {\n type DiagnoseOptions,\n type ValidationReport,\n validate,\n} from \"./diagnose.js\";\nimport { findTag, isPlainObject, VOID } from \"./shared.js\";\n\n// Autofix for Domphy element trees. We ONLY apply transforms that are provably\n// lossless — they fix structurally-invalid input without guessing intent. Every\n// other diagnostic (missing/unstable keys, inline typography, literal colors,\n// unknown tones/tags) needs semantic intent the tree does not carry, so applying\n// a \"fix\" would corrupt the author's meaning (e.g. an index key is itself the\n// unstable-key anti-pattern). Those are returned in `report` for the model or a\n// human to resolve. The fixer set is a registry so safe transforms can be added.\n\n// Structural clone that preserves functions (reactive `(listener) => …` values)\n// by reference — a JSON clone would drop them. Primitives pass through.\nfunction cloneTree(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(cloneTree);\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key in value) out[key] = cloneTree(value[key]);\n return out;\n }\n return value;\n}\n\n/** One applied lossless fix. */\nexport interface AppliedFix {\n rule: string;\n /** Human path to the node, e.g. \"div > input\". */\n path: string;\n message: string;\n}\n\n/** Result of {@link fix}: the corrected tree, what was applied, and what remains. */\nexport interface FixResult {\n /** A deep copy of the input with lossless fixes applied (functions preserved). */\n tree: unknown;\n /** The lossless fixes that were applied. */\n applied: AppliedFix[];\n /** validate() run on the fixed tree — `report.issues` are the manual remainder. */\n report: ValidationReport;\n}\n\n/**\n * Applies every provably-lossless fix to a copy of the tree and returns the\n * result plus a fresh validation report. Currently fixes `void-content` (a void\n * tag like input/img/br cannot have children, so its content is set to null).\n * Issues that need intent are left untouched and surface in `report` — this\n * includes `raw-spacing-value` and `raw-theme-value` (require semantic choices)\n * and key rules (require stable identity from data, not the tree shape).\n */\nexport function fix(root: unknown, options: DiagnoseOptions = {}): FixResult {\n const tree = cloneTree(root);\n const applied: AppliedFix[] = [];\n walkFix(tree, \"\", applied);\n return { tree, applied, report: validate(tree, options) };\n}\n\nfunction walkFix(node: unknown, path: string, applied: AppliedFix[]): void {\n if (Array.isArray(node)) {\n for (const [index, child] of node.entries()) {\n walkFix(child, `${path}[${index}]`, applied);\n }\n return;\n }\n if (!isPlainObject(node)) return;\n\n const tag = findTag(node);\n if (!tag) return;\n const here = path ? `${path} > ${tag}` : tag;\n\n // void-content: a void tag renders no children, so any content is invalid and\n // cannot be rendered — clearing it to null is lossless.\n if (VOID.has(tag) && node[tag] !== null && node[tag] !== undefined) {\n node[tag] = null;\n applied.push({\n rule: \"void-content\",\n path: here,\n message: `Void tag <${tag}> cannot have content — cleared to null.`,\n });\n }\n\n walkFix(node[tag], here, applied);\n}\n"],"mappings":"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","names":["src_exports","__export","diagnose","fix","format","validate","__toCommonJS","import_palette","import_core","TAGS","VOID","isPlainObject","value","findTag","element","key","RESERVED","TYPOGRAPHY_STYLE","COLOR_STYLE","LITERAL_COLOR","SPACING_STYLE","LITERAL_SPACING","parseOffset","value","m","isValidTone","parsed","parseLiteralToLch","trimmed","rgb","hex","lab","lch","e","EMBEDDED_COLOR","extractColorLiteral","match","buildColorHint","L","C","h","rawOffset","offset","toneStr","colorFamily","buildSpacingHint","prop","amount","unit","n","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","_b","result","elementItems","child","isPlainObject","findTag","item","index","seenKeys","key","literalKey","count","element","tag","here","contentKeys","content","VOID","style","colorLiteral","colorHint","spacingHint","dataTone","dataDensity","dataSize","validate","issues","summary","issue","format","diagnostics","icon","s","d","cloneTree","value","isPlainObject","out","key","fix","root","options","tree","applied","walkFix","validate","node","path","index","child","tag","findTag","here","VOID"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/diagnose.ts","../src/shared.ts","../src/fix.ts"],"sourcesContent":["// @domphy/doctor — static analyzer for Domphy element trees. Catches\n// non-idiomatic patterns (inline typography, literal theme colors, unknown\n// tones, void-tag content, missing/duplicate/unstable _key on lists, unknown\n// tags) so humans and AI agents get a feedback loop to self-correct generated\n// code. `validate()` is the aggregate entry point.\n\nexport type {\n CustomRule,\n DiagnoseOptions,\n Diagnostic,\n RuleCategory,\n Severity,\n ValidationReport,\n ValidationSummary,\n} from \"./diagnose.js\";\nexport { diagnose, format, validate } from \"./diagnose.js\";\nexport type { AppliedFix, FixResult } from \"./fix.js\";\nexport { fix } from \"./fix.js\";\n","import { cssRgbToRgb, hexToRgb, labToLch, rgbToLab } from \"@domphy/palette\";\nimport { findTag, isPlainObject, VOID } from \"./shared.js\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\n/**\n * Broad structural category for a rule. Mirrors Biome's lint category model.\n * Built-in rules always set this; custom rules may omit it.\n */\nexport type RuleCategory =\n | \"structure\" // void-content, unknown-tag\n | \"key\" // missing-key, duplicate-key, unstable-key\n | \"theme\" // raw-theme-value, raw-spacing-value\n | \"typography\" // inline-typography\n | \"data-attr\"; // unknown-tone, middle-surface-anchor, unknown-density, unknown-size\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /**\n * Broad structural category. Built-in rules always set this.\n * Custom rules may omit it.\n */\n category?: RuleCategory;\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\n/**\n * A custom rule that extends the doctor with project-specific checks.\n * Custom rules run alongside the 12 built-in rules; their ids must not\n * clash with any built-in id.\n *\n * @example\n * ```ts\n * const noEmptyContent: CustomRule = {\n * id: \"no-empty-content\",\n * severity: \"warning\",\n * category: \"structure\",\n * check: (element, path, tag) => {\n * if (element[tag] === \"\") {\n * return [{ message: `Empty string on <${tag}> — use null or text.` }]\n * }\n * return []\n * },\n * }\n *\n * diagnose(tree, { rules: [noEmptyContent] })\n * ```\n */\nexport interface CustomRule {\n /** Unique id shown in diagnostics. Must not clash with any built-in rule id. */\n id: string;\n /** Default severity for violations produced by this rule. */\n severity: Severity;\n /** Category for display and filtering. Optional. */\n category?: RuleCategory;\n /**\n * Called once per element node (nodes that have a valid HTML/SVG tag).\n * Return an array of violation descriptors. The engine fills in `rule`,\n * `severity`, `category`, and `path`; provide `message` and optionally\n * `hint`. Pass `severity` in the descriptor to override the rule default.\n */\n check: (\n element: Record<string, unknown>,\n path: string,\n tag: string,\n ) => Array<{ message: string; hint?: string; severity?: Severity }>;\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 * If set, only emit diagnostics whose rule id is in this list.\n * Takes precedence over `exclude`.\n * Applies to both built-in and custom rules.\n */\n only?: string[];\n /**\n * Rule ids to skip entirely.\n * Ignored when `only` is also set.\n * Applies to both built-in and custom rules.\n */\n exclude?: string[];\n /**\n * Additional custom rules to run alongside the 12 built-in rules.\n * Custom rule ids are also subject to `only`/`exclude` filtering.\n */\n rules?: CustomRule[];\n}\n\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n \"_doctorDisable\", // suppress annotation — treated as metadata, not a tag candidate\n]);\n\n// Typography style properties that must not be set inline — use patches instead.\n// Expanded from bench data: fontFamily + textDecoration were missing and caused\n// agents to write { style: { fontFamily: \"...\" } } without correction.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n \"fontFamily\",\n \"textDecoration\",\n]);\n\n// Color-bearing style props that should resolve through a theme token rather\n// than a literal value, so theming and dark mode apply. Shorthands\n// (background/border/outline) are included because they often carry a color.\nconst COLOR_STYLE = new Set([\n \"color\",\n \"backgroundColor\",\n \"background\",\n \"borderColor\",\n \"border\",\n \"outlineColor\",\n \"outline\",\n \"fill\",\n \"stroke\",\n]);\n\n// Direct (non-shorthand) color-only style properties. For these, ANY plain\n// string value that is not a CSS function or semantic keyword is treated as a\n// raw color — not just hex/rgb, but also named CSS colors like \"red\" or \"white\"\n// that agents frequently write instead of themeColor().\nconst DIRECT_COLOR_PROPS = new Set([\n \"color\",\n \"fill\",\n \"stroke\",\n \"backgroundColor\",\n \"outlineColor\",\n \"borderColor\",\n \"caretColor\",\n \"accentColor\",\n \"columnRuleColor\",\n \"textDecorationColor\",\n]);\n\n// CSS keyword values that carry no color meaning. These must never be flagged\n// even though they appear on color properties.\nconst CSS_SEMANTIC_VALUES = new Set([\n \"transparent\",\n \"currentcolor\",\n \"inherit\",\n \"initial\",\n \"unset\",\n \"none\",\n \"auto\",\n \"revert\",\n \"revert-layer\",\n \"\",\n]);\n\n// A literal color value: hex (#rgb … #rrggbbaa) or an rgb()/rgba()/hsl()/hsla()\n// function. Keywords like transparent/currentColor/inherit are intentionally\n// allowed — they carry no theme meaning.\nconst LITERAL_COLOR = /#[0-9a-fA-F]{3,8}\\b|\\b(?:rgba?|hsla?)\\s*\\(/;\n\n// Spacing style properties where literal rem/em/px values should use themeSpacing().\n// These are layout, not typography, but themeSpacing() ensures density consistency.\n// Logical properties (paddingBlock, paddingInline, etc.) are included — they are\n// used in Domphy patches and must also go through themeSpacing() for density scaling.\nconst SPACING_STYLE = new Set([\n \"margin\",\n \"marginTop\",\n \"marginRight\",\n \"marginBottom\",\n \"marginLeft\",\n \"marginInline\",\n \"marginBlock\",\n \"marginInlineStart\",\n \"marginInlineEnd\",\n \"marginBlockStart\",\n \"marginBlockEnd\",\n \"padding\",\n \"paddingTop\",\n \"paddingRight\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"paddingInline\",\n \"paddingBlock\",\n \"paddingInlineStart\",\n \"paddingInlineEnd\",\n \"paddingBlockStart\",\n \"paddingBlockEnd\",\n \"gap\",\n \"rowGap\",\n \"columnGap\",\n]);\n\n// Matches literal spacing values like \"16px\", \"1.5rem\", \"2em\" but not \"auto\",\n// \"inherit\", \"0\" (unitless zero is fine), or computed values.\nconst LITERAL_SPACING = /^(\\d+(?:\\.\\d+)?)(rem|em|px)$/;\n\n// Parses \"increase-N\" / \"decrease-N\" / \"shift-N\" into family + numeric offset.\n// Returns null when the pattern doesn't match (grammar error).\nfunction parseOffset(\n value: string,\n): { family: \"increase\" | \"decrease\" | \"shift\"; n: number } | null {\n const m = value.match(/^(increase|decrease|shift)-(\\d+)$/);\n if (!m) return null;\n return {\n family: m[1] as \"increase\" | \"decrease\" | \"shift\",\n n: parseInt(m[2], 10),\n };\n}\n\n// Valid `dataTone` grammar AND range:\n// \"inherit\", \"base\", a bare integer, or shift-N/increase-N/decrease-N where N ≤ 17.\n// The default Domphy theme has 18 tone steps (0–17). Values with valid grammar\n// but N > 17 are also rejected here so they surface as `unknown-tone` errors.\nfunction isValidTone(value: string): boolean {\n if (value === \"inherit\" || value === \"base\") return true;\n if (/^-?\\d+$/.test(value)) return true;\n const parsed = parseOffset(value);\n if (!parsed) return false;\n return parsed.n <= 17; // tone ramp has 18 steps: 0–17\n}\n\n// ─── Chromametry integration ─────────────────────────────────────────────────\n\n/**\n * Parses a CSS color literal (hex or rgb/rgba) into LCH [L, C, h].\n * Returns null if parsing fails or the format is unsupported (named colors, hsl).\n * Uses @domphy/palette math (CIELAB via D65 reference white).\n */\nfunction parseLiteralToLch(value: string): [number, number, number] | null {\n try {\n const trimmed = value.trim();\n let rgb: number[];\n\n if (trimmed.startsWith(\"#\")) {\n let hex = trimmed;\n if (hex.length === 9) hex = hex.slice(0, 7); // strip alpha #rrggbbaa → #rrggbb\n if (hex.length === 5) hex = hex.slice(0, 4); // strip alpha #rgba → #rgb\n if (hex.length === 4) {\n hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;\n }\n if (hex.length !== 7) return null;\n rgb = hexToRgb(hex);\n } else if (/^rgba?\\s*\\(/.test(trimmed)) {\n rgb = cssRgbToRgb(trimmed);\n } else {\n return null; // hsl, named colors, custom-properties — skip\n }\n\n const lab = rgbToLab(rgb);\n const lch = labToLch(lab);\n return [lch[0], lch[1], lch[2]];\n } catch {\n return null;\n }\n}\n\n// Pulls the first parseable color token out of a (possibly shorthand) value.\n// Shorthands such as \"1px solid #ccc\" or \"0 0 4px rgba(0,0,0,.5)\" embed the\n// color among other tokens, so `parseLiteralToLch` (which expects the value to\n// START with the color) would otherwise miss it. Matches a #hex literal or a\n// complete rgb()/rgba() call (with its arguments). hsl()/named colors are left\n// for the generic fallback, matching parseLiteralToLch's own coverage.\nconst EMBEDDED_COLOR = /#[0-9a-fA-F]{3,8}\\b|rgba?\\s*\\([^)]*\\)/;\n\nfunction extractColorLiteral(value: string): string | null {\n const match = EMBEDDED_COLOR.exec(value);\n return match ? match[0] : null;\n}\n\n/**\n * Converts LCH coordinates into a concrete `themeColor()` call suggestion plus\n * a perceptual description. The tone and color-family are approximations for the\n * default Domphy theme (light, 10 neutral tones, base at mid-lightness).\n */\nfunction buildColorHint(lch: [number, number, number]): string {\n const [L, C, h] = lch;\n\n // Map lightness to a Domphy tone relative to base (~L50).\n // Each step ≈ 10 lightness units — clamp to ±9 (max offset in a 10-step ramp).\n const rawOffset = Math.round((L - 50) / 10);\n const offset = Math.max(-9, Math.min(9, rawOffset));\n let toneStr: string;\n if (Math.abs(offset) <= 1) toneStr = '\"base\"';\n else if (offset < 0) toneStr = `\"decrease-${Math.abs(offset)}\"`;\n else toneStr = `\"increase-${offset}\"`;\n\n // Infer the most likely semantic color family from chroma + hue.\n let colorFamily: string;\n if (C < 12) colorFamily = \"neutral\";\n else if (h < 30 || h >= 330)\n colorFamily = \"error\"; // red spectrum\n else if (h < 75)\n colorFamily = \"warning\"; // orange-yellow\n else if (h < 165)\n colorFamily = \"success\"; // green\n else if (h < 265)\n colorFamily = \"primary\"; // blue-indigo\n else colorFamily = \"primary\"; // violet → treat as primary\n\n return (\n `(l) => themeColor(l, ${toneStr}, \"${colorFamily}\") ` +\n `[perceptual LCH L=${Math.round(L)} C=${Math.round(C)} h=${Math.round(h)}°]`\n );\n}\n\n/**\n * Converts a literal spacing value like \"16px\" / \"1.5rem\" / \"2em\" into a\n * themeSpacing(n) suggestion. themeSpacing(n) = n/4 em, so n=4 → 1em ≈ 16px.\n */\nfunction buildSpacingHint(prop: string, value: string): string | null {\n const match = LITERAL_SPACING.exec(value);\n if (!match) return null;\n const amount = parseFloat(match[1]);\n const unit = match[2];\n let n: number;\n if (unit === \"rem\" || unit === \"em\") {\n n = Math.round(amount * 4);\n } else {\n // px: assume default 16px/rem → 1em = 16px\n n = Math.round(amount / 4);\n }\n if (n <= 0) return null;\n return `${prop}: themeSpacing(${n}) — themeSpacing(n)=n/4em, so ${n}/4=${n / 4}em ≈ ${value} at default density`;\n}\n\n// ─── Suppress helper ─────────────────────────────────────────────────────────\n\n/**\n * Applies `_doctorDisable` filtering. `elementDiags` are diagnostics produced\n * directly by this element's checks (always subject to suppression). `contentDiags`\n * are diagnostics produced by walking the element's reactive content (array-level\n * rules like missing-key fire at the element's path, so they are also suppressed\n * when they match `here`; deeper-nested diagnostics pass through unconditionally).\n */\nfunction applyDisable(\n disable: unknown,\n elementDiags: Diagnostic[],\n contentDiags: Diagnostic[],\n here: string,\n out: Diagnostic[],\n): void {\n if (disable === true) {\n // Suppress all diagnostics at this path (element-level + array-level).\n // Let diagnostics from deeper nodes through unconditionally.\n for (const d of contentDiags) {\n if (d.path !== here) out.push(d);\n }\n return;\n }\n if (disable !== undefined && disable !== null && disable !== false) {\n const disabled = new Set(\n Array.isArray(disable) ? (disable as string[]) : [String(disable)],\n );\n for (const d of elementDiags) {\n if (!disabled.has(d.rule)) out.push(d);\n }\n for (const d of contentDiags) {\n // Only suppress at THIS element's path; deeper diagnostics pass through.\n if (d.path === here && disabled.has(d.rule)) continue;\n out.push(d);\n }\n return;\n }\n // No disable — pass everything through.\n out.push(...elementDiags);\n out.push(...contentDiags);\n}\n\n// ─── Tree walkers ─────────────────────────────────────────────────────────────\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);\n\n // Apply only/exclude post-filter (covers both built-in and custom rule ids).\n // `only` being set (even empty) activates whitelist mode: only listed rule ids pass.\n if (options.only !== undefined) {\n if (options.only.length === 0) return [];\n const only = new Set(options.only);\n return out.filter((d) => only.has(d.rule));\n }\n if (options.exclude && options.exclude.length > 0) {\n const exclude = new Set(options.exclude);\n return out.filter((d) => !exclude.has(d.rule));\n }\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n options: DiagnoseOptions,\n): void {\n const runReactive = options.runReactive !== false;\n\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, options);\n return;\n }\n\n if (Array.isArray(node)) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n\n if (dynamic) {\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 category: \"key\",\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 // unstable-key (heuristic): in a dynamic list every `_key` equals its\n // sibling position (0, 1, 2, …). That is the runtime footprint of\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\n // defeats the point of keying because keys shift when the list reorders.\n if (\n elementItems.length > 1 &&\n elementItems.every((item, index) => item._key === index)\n ) {\n out.push({\n rule: \"unstable-key\",\n severity: \"warning\",\n category: \"key\",\n path: path || \"(list)\",\n message:\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\n });\n }\n }\n\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\n const seenKeys = new Map<string, number>();\n for (const item of elementItems) {\n const key = item._key;\n if (key === undefined || key === null) continue;\n const literalKey = `${typeof key}:${String(key)}`;\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\n }\n for (const [literalKey, count] of seenKeys) {\n if (count > 1) {\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\n out.push({\n rule: \"duplicate-key\",\n severity: \"error\",\n category: \"key\",\n path: path || \"(list)\",\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\n });\n }\n }\n\n node.forEach((child, index) => {\n walk(child, `${path}[${index}]`, out, false, options);\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 // Collect element-level diagnostics in a local buffer so `_doctorDisable`\n // can filter them before they reach `out`.\n const elementDiags: Diagnostic[] = [];\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 elementDiags.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n category: \"structure\",\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 applyDisable(element._doctorDisable, elementDiags, [], here, out);\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n elementDiags.push({\n rule: \"void-content\",\n severity: \"error\",\n category: \"structure\",\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 const value = style[prop];\n\n // inline-typography: typography properties must come from patches, not\n // inline style. fontFamily and textDecoration were missing from the original\n // set and are added here based on bench data showing persistent violations.\n if (TYPOGRAPHY_STYLE.has(prop) && typeof value !== \"function\") {\n elementDiags.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n category: \"typography\",\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 // raw-theme-value: literal color values bypass theming/dark mode.\n // Enhanced with @domphy/palette chromametry: converts the literal color to\n // LCH and suggests the nearest themeColor() call with perceptual coordinates.\n if (\n COLOR_STYLE.has(prop) &&\n typeof value === \"string\" &&\n LITERAL_COLOR.test(value)\n ) {\n // For shorthands (border/outline/background) the color is embedded in a\n // larger string, so extract the color token first; fall back to the\n // whole value (covers the simple `color: \"#fff\"` case).\n const colorLiteral = extractColorLiteral(value) ?? value;\n const lch = parseLiteralToLch(colorLiteral);\n const colorHint = lch\n ? buildColorHint(lch)\n : \"(l) => themeColor(l, tone, colorName)\";\n\n elementDiags.push({\n rule: \"raw-theme-value\",\n severity: \"info\",\n category: \"theme\",\n path: here,\n message: `Inline \\`${prop}\\` uses a literal color (${value}).`,\n hint: `Prefer a theme token — ${colorHint} — so theming and dark mode apply.`,\n });\n }\n\n // raw-theme-value (named colors): for direct color-only properties, also\n // flag CSS named colors like \"red\" or \"white\" that bypass the theme system.\n // The hex/rgb check above misses these because LITERAL_COLOR only matches\n // hex and function-style values. Shorthands (border, background, outline)\n // are excluded — color extraction from shorthands is already handled above.\n if (\n DIRECT_COLOR_PROPS.has(prop) &&\n typeof value === \"string\" &&\n !LITERAL_COLOR.test(value) && // not already caught by the hex/rgb check\n !value.includes(\"(\") && // not a CSS function (var, calc, rgb, gradient…)\n !value.startsWith(\"--\") && // not a custom property reference\n !CSS_SEMANTIC_VALUES.has(value.trim().toLowerCase())\n ) {\n elementDiags.push({\n rule: \"raw-theme-value\",\n severity: \"info\",\n category: \"theme\",\n path: here,\n message: `Inline \\`${prop}\\` uses a CSS named color (\"${value}\").`,\n hint: `CSS named colors like \"${value}\" bypass theming and dark mode. Prefer (l) => themeColor(l, tone, colorName) — so the theme context applies.`,\n });\n }\n\n // raw-spacing-value: literal rem/em/px spacing values should use themeSpacing()\n // to respect the theme's density system. info-severity (soft recommendation).\n if (SPACING_STYLE.has(prop) && typeof value === \"string\") {\n const spacingHint = buildSpacingHint(prop, value);\n if (spacingHint) {\n elementDiags.push({\n rule: \"raw-spacing-value\",\n severity: \"info\",\n category: \"theme\",\n path: here,\n message: `Inline \\`${prop}: \"${value}\"\\` uses a literal spacing value.`,\n hint: `Prefer themeSpacing() for theme density: ${spacingHint}`,\n });\n }\n }\n }\n }\n\n // tone-background-inherit: backgroundColor should always resolve to the current\n // surface tone via themeColor(l, \"inherit\"), not a fixed shifted tone.\n // Detected by running the reactive function at context=0 (no-op listener\n // has no elementNode → contextTone returns 0): if the result is a\n // var(--X-N) reference with N > 0, the function uses a non-inherit tone.\n // This catches backgroundColor: (l) => themeColor(l, \"shift-N\") — which\n // double-shifts when the element itself also has dataTone set, but is also\n // wrong in general: use dataTone to shift the surface, not backgroundColor.\n const bgProp = isPlainObject(element.style)\n ? (element.style as Record<string, unknown>).backgroundColor\n : undefined;\n if (typeof bgProp === \"function\" && runReactive) {\n let bgResult: unknown;\n try {\n bgResult = (bgProp as (l: unknown) => unknown)(() => {});\n } catch {\n // reactive fn threw without a real runtime — skip\n }\n if (typeof bgResult === \"string\") {\n const bgMatch = bgResult.match(/var\\(--[\\w-]+-(\\d+)\\)$/);\n if (bgMatch && parseInt(bgMatch[1], 10) > 0) {\n elementDiags.push({\n rule: \"tone-background-inherit\",\n severity: \"warning\",\n category: \"theme\",\n path: here,\n message: `\\`style.backgroundColor\\` uses a fixed tone (resolves to \"${bgResult}\" at base context) instead of \"inherit\".`,\n hint: 'backgroundColor should always be (l) => themeColor(l, \"inherit\"). To shift the surface tone, set dataTone on the container — it applies to all children uniformly.',\n });\n }\n }\n }\n\n // low-contrast: detect insufficient contrast between `color` and `backgroundColor`\n // by comparing their shift numbers extracted from the `var(--X-N)` strings that\n // themeColor() returns. Both props must be reactive and resolve to theme vars for\n // this check to fire. A shift difference < 9 violates WCAG-level legibility.\n {\n const styleProp = isPlainObject(element.style)\n ? (element.style as Record<string, unknown>)\n : null;\n const colorFn = styleProp?.color;\n const bgFn = styleProp?.backgroundColor;\n\n if (runReactive && typeof colorFn === \"function\" && typeof bgFn === \"function\") {\n let colorVar: unknown;\n let bgVar: unknown;\n try {\n colorVar = (colorFn as (l: unknown) => unknown)(() => {});\n bgVar = (bgFn as (l: unknown) => unknown)(() => {});\n } catch { /* reactive fn threw without a runtime — skip */ }\n\n const extractShift = (v: unknown): number | null => {\n if (typeof v !== \"string\") return null;\n const match = v.match(/var\\(--[\\w-]+-(\\d+)\\)$/);\n return match ? parseInt(match[1], 10) : null;\n };\n\n const textShift = extractShift(colorVar);\n const bgShift = extractShift(bgVar);\n\n if (textShift !== null && bgShift !== null) {\n const diff = Math.abs(textShift - bgShift);\n if (diff < 9) {\n elementDiags.push({\n rule: \"low-contrast\",\n severity: \"warning\",\n category: \"theme\",\n path: here,\n message: `Text/background shift gap is ${diff} (shift-${textShift} vs shift-${bgShift}) — contrast may be insufficient.`,\n hint: `Aim for ≥9 shift steps between text and surface. E.g. shift-0 bg + shift-9 text, or shift-11 text on a shift-0 surface. Increase the gap or rely on a parent dataTone to open it.`,\n });\n }\n }\n }\n }\n\n // unknown-tone: dataTone is not valid grammar, or it's valid grammar but the\n // numeric offset is out of the 18-step ramp range (0–17).\n const dataTone = element.dataTone;\n if (typeof dataTone === \"string\") {\n if (!isValidTone(dataTone)) {\n elementDiags.push({\n rule: \"unknown-tone\",\n severity: \"warning\",\n category: \"data-attr\",\n path: here,\n message: `\\`dataTone\\` \"${dataTone}\" is not a valid tone.`,\n hint: 'Use \"inherit\", \"base\", a number, or \"shift-N\"/\"increase-N\"/\"decrease-N\" with N ≤ 17 (the ramp has 18 steps). Words like \"surface\"/\"text\" are not tones.',\n });\n } else {\n // middle-surface-anchor: shift-4 through shift-13 sets a mid-ramp surface\n // anchor. Children's tones may clamp and fold back, collapsing the contrast\n // between background and text. Edge anchors (0–3 light, 14–17 dark) are safe.\n const parsed = parseOffset(dataTone);\n if (parsed?.family === \"shift\" && parsed.n >= 4 && parsed.n <= 13) {\n elementDiags.push({\n rule: \"middle-surface-anchor\",\n severity: \"warning\",\n category: \"data-attr\",\n path: here,\n message: `\\`dataTone: \"${dataTone}\"\\` uses a mid-ramp surface anchor (steps 4–13). Child tones derived from this surface may clamp and collapse contrast.`,\n hint: \"Prefer edge anchors: shift-0–3 for light surfaces, shift-14–17 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions.\",\n });\n }\n }\n }\n\n // unknown-density: dataDensity value is invalid grammar or out of the 5-step\n // density range (increase/decrease 0–4; the scale factors are 0.75, 1, 1.5, 2, 2.5).\n const dataDensity = element.dataDensity;\n if (typeof dataDensity === \"string\" && dataDensity !== \"inherit\") {\n const parsed = parseOffset(dataDensity);\n if (!parsed || parsed.family === \"shift\") {\n elementDiags.push({\n rule: \"unknown-density\",\n severity: \"warning\",\n category: \"data-attr\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" is not a valid density offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–4. \"shift-\" is not valid for density.',\n });\n } else if (parsed.n > 4) {\n elementDiags.push({\n rule: \"unknown-density\",\n severity: \"error\",\n category: \"data-attr\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" N=${parsed.n} is out of range — the density scale has 5 steps (max offset: 4).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 4. Density factors: [0.75, 1, 1.5, 2, 2.5].',\n });\n }\n }\n\n // unknown-size: dataSize value is invalid grammar or out of the 8-step size\n // range (increase/decrease 0–7).\n const dataSize = element.dataSize;\n if (typeof dataSize === \"string\" && dataSize !== \"inherit\") {\n const parsed = parseOffset(dataSize);\n if (!parsed || parsed.family === \"shift\") {\n elementDiags.push({\n rule: \"unknown-size\",\n severity: \"warning\",\n category: \"data-attr\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" is not a valid size offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–7. \"shift-\" is not valid for size.',\n });\n } else if (parsed.n > 7) {\n elementDiags.push({\n rule: \"unknown-size\",\n severity: \"error\",\n category: \"data-attr\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" N=${parsed.n} is out of range — the size scale has 8 steps (max offset: 7).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 7.',\n });\n }\n }\n\n // Custom rules: run each user-provided rule against this element.\n if (options.rules && options.rules.length > 0) {\n for (const rule of options.rules) {\n let violations: ReturnType<CustomRule[\"check\"]>;\n try {\n violations = rule.check(element, here, tag);\n } catch {\n continue; // custom rule threw — skip silently\n }\n for (const v of violations) {\n elementDiags.push({\n rule: rule.id,\n severity: v.severity ?? rule.severity,\n category: rule.category,\n path: here,\n message: v.message,\n hint: v.hint,\n });\n }\n }\n }\n\n // Walk the element's content into a separate buffer so _doctorDisable can\n // filter array-level diagnostics (missing-key / duplicate-key / etc.) that\n // fire at THIS element's path when the content is a reactive function.\n const contentDiags: Diagnostic[] = [];\n walk(content, here, contentDiags, false, options);\n\n // Apply _doctorDisable and flush into the shared output.\n applyDisable(element._doctorDisable, elementDiags, contentDiags, here, out);\n}\n\n/** Issue counts by severity, plus the grand total. */\nexport interface ValidationSummary {\n error: number;\n warning: number;\n info: number;\n total: number;\n}\n\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\nexport interface ValidationReport {\n /** True when there are no `error`-severity diagnostics. */\n ok: boolean;\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\n issues: Diagnostic[];\n summary: ValidationSummary;\n}\n\n/**\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\n * the issue list, and counts by severity). `ok` is false when any `error`\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\n */\nexport function validate(\n root: unknown,\n options: DiagnoseOptions = {},\n): ValidationReport {\n const issues = diagnose(root, options);\n const summary: ValidationSummary = {\n error: 0,\n warning: 0,\n info: 0,\n total: issues.length,\n };\n for (const issue of issues) summary[issue.severity] += 1;\n return { ok: summary.error === 0, issues, summary };\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","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\n\n// Internal helpers shared by diagnose.ts and fix.ts. Kept in one module so the\n// tag tables and the tree-shape predicates have a single source of truth.\n\n/** Every valid HTML and SVG tag name. */\nexport const TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\n\n/** Tags that render no children (input, img, br, …). */\nexport const VOID = new Set<string>(VoidTags);\n\n/** True for a non-array object (a Domphy element or a plain record). */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Returns the element's tag key (the first key that names a valid tag). */\nexport function 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","import {\n type DiagnoseOptions,\n type ValidationReport,\n validate,\n} from \"./diagnose.js\";\nimport { findTag, isPlainObject, VOID } from \"./shared.js\";\n\n// Autofix for Domphy element trees. We ONLY apply transforms that are provably\n// lossless — they fix structurally-invalid input without guessing intent. Every\n// other diagnostic (missing/unstable keys, inline typography, literal colors,\n// unknown tones/tags) needs semantic intent the tree does not carry, so applying\n// a \"fix\" would corrupt the author's meaning (e.g. an index key is itself the\n// unstable-key anti-pattern). Those are returned in `report` for the model or a\n// human to resolve. The fixer set is a registry so safe transforms can be added.\n\n// Structural clone that preserves functions (reactive `(listener) => …` values)\n// by reference — a JSON clone would drop them. Primitives pass through.\nfunction cloneTree(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(cloneTree);\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key in value) out[key] = cloneTree(value[key]);\n return out;\n }\n return value;\n}\n\n/** One applied lossless fix. */\nexport interface AppliedFix {\n rule: string;\n /** Human path to the node, e.g. \"div > input\". */\n path: string;\n message: string;\n}\n\n/** Result of {@link fix}: the corrected tree, what was applied, and what remains. */\nexport interface FixResult {\n /** A deep copy of the input with lossless fixes applied (functions preserved). */\n tree: unknown;\n /** The lossless fixes that were applied. */\n applied: AppliedFix[];\n /** validate() run on the fixed tree — `report.issues` are the manual remainder. */\n report: ValidationReport;\n}\n\n/**\n * Applies every provably-lossless fix to a copy of the tree and returns the\n * result plus a fresh validation report. Currently fixes `void-content` (a void\n * tag like input/img/br cannot have children, so its content is set to null).\n * Issues that need intent are left untouched and surface in `report` — this\n * includes `raw-spacing-value` and `raw-theme-value` (require semantic choices)\n * and key rules (require stable identity from data, not the tree shape).\n */\nexport function fix(root: unknown, options: DiagnoseOptions = {}): FixResult {\n const tree = cloneTree(root);\n const applied: AppliedFix[] = [];\n walkFix(tree, \"\", applied);\n return { tree, applied, report: validate(tree, options) };\n}\n\nfunction walkFix(node: unknown, path: string, applied: AppliedFix[]): void {\n if (Array.isArray(node)) {\n for (const [index, child] of node.entries()) {\n walkFix(child, `${path}[${index}]`, applied);\n }\n return;\n }\n if (!isPlainObject(node)) return;\n\n const tag = findTag(node);\n if (!tag) return;\n const here = path ? `${path} > ${tag}` : tag;\n\n // void-content: a void tag renders no children, so any content is invalid and\n // cannot be rendered — clearing it to null is lossless.\n if (VOID.has(tag) && node[tag] !== null && node[tag] !== undefined) {\n node[tag] = null;\n applied.push({\n rule: \"void-content\",\n path: here,\n message: `Void tag <${tag}> cannot have content — cleared to null.`,\n });\n }\n\n walkFix(node[tag], here, applied);\n}\n"],"mappings":"yaAAA,IAAAA,GAAA,GAAAC,EAAAD,GAAA,cAAAE,EAAA,QAAAC,EAAA,WAAAC,EAAA,aAAAC,IAAA,eAAAC,EAAAN,ICAA,IAAAO,EAA0D,2BCA1D,IAAAC,EAA4C,wBAM/BC,EAAO,IAAI,IAAY,CAAC,GAAG,WAAU,GAAG,SAAO,CAAC,EAGhDC,EAAO,IAAI,IAAY,UAAQ,EAGrC,SAASC,EACdC,EACkC,CAClC,OAAO,OAAOA,GAAU,UAAYA,IAAU,MAAQ,CAAC,MAAM,QAAQA,CAAK,CAC5E,CAGO,SAASC,EAAQC,EAAsD,CAC5E,QAAWC,KAAOD,EAChB,GAAIL,EAAK,IAAIM,CAAG,EAAG,OAAOA,CAG9B,CD4EA,IAAMC,EAAW,IAAI,IAAI,CACvB,IACA,QACA,OACA,UACA,WACA,YACA,gBACF,CAAC,EAKKC,EAAmB,IAAI,IAAI,CAC/B,WACA,aACA,aACA,gBACA,aACA,gBACF,CAAC,EAKKC,GAAc,IAAI,IAAI,CAC1B,QACA,kBACA,aACA,cACA,SACA,eACA,UACA,OACA,QACF,CAAC,EAMKC,GAAqB,IAAI,IAAI,CACjC,QACA,OACA,SACA,kBACA,eACA,cACA,aACA,cACA,kBACA,qBACF,CAAC,EAIKC,GAAsB,IAAI,IAAI,CAClC,cACA,eACA,UACA,UACA,QACA,OACA,OACA,SACA,eACA,EACF,CAAC,EAKKC,EAAgB,6CAMhBC,GAAgB,IAAI,IAAI,CAC5B,SACA,YACA,cACA,eACA,aACA,eACA,cACA,oBACA,kBACA,mBACA,iBACA,UACA,aACA,eACA,gBACA,cACA,gBACA,eACA,qBACA,mBACA,oBACA,kBACA,MACA,SACA,WACF,CAAC,EAIKC,GAAkB,+BAIxB,SAASC,EACPC,EACiE,CACjE,IAAMC,EAAID,EAAM,MAAM,mCAAmC,EACzD,OAAKC,EACE,CACL,OAAQA,EAAE,CAAC,EACX,EAAG,SAASA,EAAE,CAAC,EAAG,EAAE,CACtB,EAJe,IAKjB,CAMA,SAASC,GAAYF,EAAwB,CAE3C,GADIA,IAAU,WAAaA,IAAU,QACjC,UAAU,KAAKA,CAAK,EAAG,MAAO,GAClC,IAAMG,EAASJ,EAAYC,CAAK,EAChC,OAAKG,EACEA,EAAO,GAAK,GADC,EAEtB,CASA,SAASC,GAAkBJ,EAAgD,CACzE,GAAI,CACF,IAAMK,EAAUL,EAAM,KAAK,EACvBM,EAEJ,GAAID,EAAQ,WAAW,GAAG,EAAG,CAC3B,IAAIE,EAAMF,EAMV,GALIE,EAAI,SAAW,IAAGA,EAAMA,EAAI,MAAM,EAAG,CAAC,GACtCA,EAAI,SAAW,IAAGA,EAAMA,EAAI,MAAM,EAAG,CAAC,GACtCA,EAAI,SAAW,IACjBA,EAAM,IAAIA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,IAE3DA,EAAI,SAAW,EAAG,OAAO,KAC7BD,KAAM,YAASC,CAAG,CACpB,SAAW,cAAc,KAAKF,CAAO,EACnCC,KAAM,eAAYD,CAAO,MAEzB,QAAO,KAGT,IAAMG,KAAM,YAASF,CAAG,EAClBG,KAAM,YAASD,CAAG,EACxB,MAAO,CAACC,EAAI,CAAC,EAAGA,EAAI,CAAC,EAAGA,EAAI,CAAC,CAAC,CAChC,OAAQC,EAAA,CACN,OAAO,IACT,CACF,CAQA,IAAMC,GAAiB,wCAEvB,SAASC,GAAoBZ,EAA8B,CACzD,IAAMa,EAAQF,GAAe,KAAKX,CAAK,EACvC,OAAOa,EAAQA,EAAM,CAAC,EAAI,IAC5B,CAOA,SAASC,GAAeL,EAAuC,CAC7D,GAAM,CAACM,EAAGC,EAAGC,CAAC,EAAIR,EAIZS,EAAY,KAAK,OAAOH,EAAI,IAAM,EAAE,EACpCI,EAAS,KAAK,IAAI,GAAI,KAAK,IAAI,EAAGD,CAAS,CAAC,EAC9CE,EACA,KAAK,IAAID,CAAM,GAAK,EAAGC,EAAU,SAC5BD,EAAS,EAAGC,EAAU,aAAa,KAAK,IAAID,CAAM,CAAC,IACvDC,EAAU,aAAaD,CAAM,IAGlC,IAAIE,EACJ,OAAIL,EAAI,GAAIK,EAAc,UACjBJ,EAAI,IAAMA,GAAK,IACtBI,EAAc,QACPJ,EAAI,GACXI,EAAc,UACPJ,EAAI,IACXI,EAAc,WACPJ,EAAI,IACXI,EAAc,WAId,wBAAwBD,CAAO,MAAMC,CAAW,wBAC3B,KAAK,MAAMN,CAAC,CAAC,MAAM,KAAK,MAAMC,CAAC,CAAC,MAAM,KAAK,MAAMC,CAAC,CAAC,OAE5E,CAMA,SAASK,GAAiBC,EAAcvB,EAA8B,CACpE,IAAMa,EAAQf,GAAgB,KAAKE,CAAK,EACxC,GAAI,CAACa,EAAO,OAAO,KACnB,IAAMW,EAAS,WAAWX,EAAM,CAAC,CAAC,EAC5BY,EAAOZ,EAAM,CAAC,EAChBa,EAOJ,OANID,IAAS,OAASA,IAAS,KAC7BC,EAAI,KAAK,MAAMF,EAAS,CAAC,EAGzBE,EAAI,KAAK,MAAMF,EAAS,CAAC,EAEvBE,GAAK,EAAU,KACZ,GAAGH,CAAI,kBAAkBG,CAAC,uCAAkCA,CAAC,MAAMA,EAAI,CAAC,aAAQ1B,CAAK,qBAC9F,CAWA,SAAS2B,EACPC,EACAC,EACAC,EACAC,EACAC,EACM,CACN,GAAIJ,IAAY,GAAM,CAGpB,QAAWK,KAAKH,EACVG,EAAE,OAASF,GAAMC,EAAI,KAAKC,CAAC,EAEjC,MACF,CACA,GAA6BL,GAAY,MAAQA,IAAY,GAAO,CAClE,IAAMM,EAAW,IAAI,IACnB,MAAM,QAAQN,CAAO,EAAKA,EAAuB,CAAC,OAAOA,CAAO,CAAC,CACnE,EACA,QAAWK,KAAKJ,EACTK,EAAS,IAAID,EAAE,IAAI,GAAGD,EAAI,KAAKC,CAAC,EAEvC,QAAWA,KAAKH,EAEVG,EAAE,OAASF,GAAQG,EAAS,IAAID,EAAE,IAAI,GAC1CD,EAAI,KAAKC,CAAC,EAEZ,MACF,CAEAD,EAAI,KAAK,GAAGH,CAAY,EACxBG,EAAI,KAAK,GAAGF,CAAY,CAC1B,CAKO,SAASK,EACdC,EACAC,EAA2B,CAAC,EACd,CACd,IAAML,EAAoB,CAAC,EAK3B,GAJAM,EAAKF,EAAM,GAAIJ,EAAK,GAAOK,CAAO,EAI9BA,EAAQ,OAAS,OAAW,CAC9B,GAAIA,EAAQ,KAAK,SAAW,EAAG,MAAO,CAAC,EACvC,IAAME,EAAO,IAAI,IAAIF,EAAQ,IAAI,EACjC,OAAOL,EAAI,OAAQC,GAAMM,EAAK,IAAIN,EAAE,IAAI,CAAC,CAC3C,CACA,GAAII,EAAQ,SAAWA,EAAQ,QAAQ,OAAS,EAAG,CACjD,IAAMG,EAAU,IAAI,IAAIH,EAAQ,OAAO,EACvC,OAAOL,EAAI,OAAQC,GAAM,CAACO,EAAQ,IAAIP,EAAE,IAAI,CAAC,CAC/C,CACA,OAAOD,CACT,CAEA,SAASM,EACPG,EACAC,EACAV,EACAW,EACAN,EACM,CA5ZR,IAAAO,EAAAC,EAAAC,EA6ZE,IAAMC,EAAcV,EAAQ,cAAgB,GAE5C,GAAI,OAAOI,GAAS,WAAY,CAC9B,GAAI,CAACM,EAAa,OAClB,IAAIC,EACJ,GAAI,CACFA,EAAUP,EAAwC,IAAM,CAAC,CAAC,CAC5D,OAAQ/B,EAAA,CACN,MACF,CACA4B,EAAKU,EAAQN,EAAMV,EAAK,GAAMK,CAAO,EACrC,MACF,CAEA,GAAI,MAAM,QAAQI,CAAI,EAAG,CACvB,IAAMQ,EAAeR,EAAK,OACvBS,GAAUC,EAAcD,CAAK,GAAKE,EAAQF,CAAK,CAClD,EAEIP,IAEAM,EAAa,OAAS,GACtBA,EAAa,KAAMI,GAASA,EAAK,OAAS,MAAS,GAEnDrB,EAAI,KAAK,CACP,KAAM,cACN,SAAU,UACV,SAAU,MACV,KAAMU,GAAQ,SACd,QACE,6GACF,KAAM,yEACR,CAAC,EAQDO,EAAa,OAAS,GACtBA,EAAa,MAAM,CAACI,EAAMC,IAAUD,EAAK,OAASC,CAAK,GAEvDtB,EAAI,KAAK,CACP,KAAM,eACN,SAAU,UACV,SAAU,MACV,KAAMU,GAAQ,SACd,QACE,2HACF,KAAM,oFACR,CAAC,GAKL,IAAMa,EAAW,IAAI,IACrB,QAAWF,KAAQJ,EAAc,CAC/B,IAAMO,EAAMH,EAAK,KACjB,GAAyBG,GAAQ,KAAM,SACvC,IAAMC,EAAa,GAAG,OAAOD,CAAG,IAAI,OAAOA,CAAG,CAAC,GAC/CD,EAAS,IAAIE,IAAab,EAAAW,EAAS,IAAIE,CAAU,IAAvB,KAAAb,EAA4B,GAAK,CAAC,CAC9D,CACA,OAAW,CAACa,EAAYC,CAAK,IAAKH,EAChC,GAAIG,EAAQ,EAAG,CACb,IAAM1D,EAAQyD,EAAW,MAAMA,EAAW,QAAQ,GAAG,EAAI,CAAC,EAC1DzB,EAAI,KAAK,CACP,KAAM,gBACN,SAAU,QACV,SAAU,MACV,KAAMU,GAAQ,SACd,QAAS,uBAAuB1C,CAAK,WAAW0D,CAAK,sDACrD,KAAM,gFACR,CAAC,CACH,CAGFjB,EAAK,QAAQ,CAACS,EAAOI,IAAU,CAC7BhB,EAAKY,EAAO,GAAGR,CAAI,IAAIY,CAAK,IAAKtB,EAAK,GAAOK,CAAO,CACtD,CAAC,EACD,MACF,CAEA,GAAI,CAACc,EAAcV,CAAI,EAAG,OAE1B,IAAMkB,EAAUlB,EACVmB,EAAMR,EAAQO,CAAO,EACrB5B,EAAO6B,EAAOlB,EAAO,GAAGA,CAAI,MAAMkB,CAAG,GAAKA,EAAOlB,GAAQ,SAIzDb,EAA6B,CAAC,EAEpC,GAAI,CAAC+B,EAAK,CACR,IAAMC,EAAc,OAAO,KAAKF,CAAO,EAAE,OACtCH,GACC,CAACjE,EAAS,IAAIiE,CAAG,GACjB,CAACA,EAAI,WAAW,KAAK,GACrB,CAACA,EAAI,WAAW,IAAI,GACpB,CAACA,EAAI,WAAW,MAAM,GACtB,CAACA,EAAI,WAAW,MAAM,CAC1B,EACIK,EAAY,SAAW,GACzBhC,EAAa,KAAK,CAChB,KAAM,cACN,SAAU,UACV,SAAU,YACV,KAAME,EACN,QAAS,IAAI8B,EAAY,CAAC,CAAC,sDAC3B,KAAM,yEACR,CAAC,EAEHlC,EAAagC,EAAQ,eAAgB9B,EAAc,CAAC,EAAGE,EAAMC,CAAG,EAChE,MACF,CAEA,IAAM8B,EAAUH,EAAQC,CAAG,EAa3B,GAXIG,EAAK,IAAIH,CAAG,GAAKE,IAAY,MAAQA,IAAY,QACnDjC,EAAa,KAAK,CAChB,KAAM,eACN,SAAU,QACV,SAAU,YACV,KAAME,EACN,QAAS,aAAa6B,CAAG,iCAAiC,MAAM,QAAQE,CAAO,EAAI,QAAU,OAAOA,CAAO,KAC3G,KAAM,WAAWF,CAAG,sDACtB,CAAC,EAGCT,EAAcQ,EAAQ,KAAK,EAAG,CAChC,IAAMK,EAAQL,EAAQ,MACtB,QAAWpC,KAAQyC,EAAO,CACxB,IAAMhE,EAAQgE,EAAMzC,CAAI,EAmBxB,GAdI/B,EAAiB,IAAI+B,CAAI,GAAK,OAAOvB,GAAU,YACjD6B,EAAa,KAAK,CAChB,KAAM,oBACN,SAAU,UACV,SAAU,aACV,KAAME,EACN,QAAS,YAAYR,CAAI,4CACzB,KAAM,gHACR,CAAC,EAOD9B,GAAY,IAAI8B,CAAI,GACpB,OAAOvB,GAAU,UACjBJ,EAAc,KAAKI,CAAK,EACxB,CAIA,IAAMiE,GAAepB,EAAAjC,GAAoBZ,CAAK,IAAzB,KAAA6C,EAA8B7C,EAC7CS,EAAML,GAAkB6D,CAAY,EACpCC,EAAYzD,EACdK,GAAeL,CAAG,EAClB,wCAEJoB,EAAa,KAAK,CAChB,KAAM,kBACN,SAAU,OACV,SAAU,QACV,KAAME,EACN,QAAS,YAAYR,CAAI,4BAA4BvB,CAAK,KAC1D,KAAM,+BAA0BkE,CAAS,yCAC3C,CAAC,CACH,CA2BA,GAnBExE,GAAmB,IAAI6B,CAAI,GAC3B,OAAOvB,GAAU,UACjB,CAACJ,EAAc,KAAKI,CAAK,GACzB,CAACA,EAAM,SAAS,GAAG,GACnB,CAACA,EAAM,WAAW,IAAI,GACtB,CAACL,GAAoB,IAAIK,EAAM,KAAK,EAAE,YAAY,CAAC,GAEnD6B,EAAa,KAAK,CAChB,KAAM,kBACN,SAAU,OACV,SAAU,QACV,KAAME,EACN,QAAS,YAAYR,CAAI,+BAA+BvB,CAAK,MAC7D,KAAM,0BAA0BA,CAAK,mHACvC,CAAC,EAKCH,GAAc,IAAI0B,CAAI,GAAK,OAAOvB,GAAU,SAAU,CACxD,IAAMmE,EAAc7C,GAAiBC,EAAMvB,CAAK,EAC5CmE,GACFtC,EAAa,KAAK,CAChB,KAAM,oBACN,SAAU,OACV,SAAU,QACV,KAAME,EACN,QAAS,YAAYR,CAAI,MAAMvB,CAAK,oCACpC,KAAM,4CAA4CmE,CAAW,EAC/D,CAAC,CAEL,CACF,CACF,CAUA,IAAMC,EAASjB,EAAcQ,EAAQ,KAAK,EACrCA,EAAQ,MAAkC,gBAC3C,OACJ,GAAI,OAAOS,GAAW,YAAcrB,EAAa,CAC/C,IAAIsB,EACJ,GAAI,CACFA,EAAYD,EAAmC,IAAM,CAAC,CAAC,CACzD,OAAQ1D,EAAA,CAER,CACA,GAAI,OAAO2D,GAAa,SAAU,CAChC,IAAMC,EAAUD,EAAS,MAAM,wBAAwB,EACnDC,GAAW,SAASA,EAAQ,CAAC,EAAG,EAAE,EAAI,GACxCzC,EAAa,KAAK,CAChB,KAAM,0BACN,SAAU,UACV,SAAU,QACV,KAAME,EACN,QAAS,6DAA6DsC,CAAQ,2CAC9E,KAAM,yKACR,CAAC,CAEL,CACF,CAMA,CACE,IAAME,EAAYpB,EAAcQ,EAAQ,KAAK,EACxCA,EAAQ,MACT,KACEa,EAAUD,GAAA,YAAAA,EAAW,MACrBE,EAAOF,GAAA,YAAAA,EAAW,gBAExB,GAAIxB,GAAe,OAAOyB,GAAY,YAAc,OAAOC,GAAS,WAAY,CAC9E,IAAIC,EACAC,EACJ,GAAI,CACFD,EAAYF,EAAoC,IAAM,CAAC,CAAC,EACxDG,EAASF,EAAiC,IAAM,CAAC,CAAC,CACpD,OAAQ/D,EAAA,CAAmD,CAE3D,IAAMkE,EAAgBC,GAA8B,CAClD,GAAI,OAAOA,GAAM,SAAU,OAAO,KAClC,IAAMhE,EAAQgE,EAAE,MAAM,wBAAwB,EAC9C,OAAOhE,EAAQ,SAASA,EAAM,CAAC,EAAG,EAAE,EAAI,IAC1C,EAEMiE,EAAYF,EAAaF,CAAQ,EACjCK,EAAUH,EAAaD,CAAK,EAElC,GAAIG,IAAc,MAAQC,IAAY,KAAM,CAC1C,IAAMC,EAAO,KAAK,IAAIF,EAAYC,CAAO,EACrCC,EAAO,GACTnD,EAAa,KAAK,CAChB,KAAM,eACN,SAAU,UACV,SAAU,QACV,KAAME,EACN,QAAS,gCAAgCiD,CAAI,WAAWF,CAAS,aAAaC,CAAO,yCACrF,KAAM,wLACR,CAAC,CAEL,CACF,CACF,CAIA,IAAME,EAAWtB,EAAQ,SACzB,GAAI,OAAOsB,GAAa,SACtB,GAAI,CAAC/E,GAAY+E,CAAQ,EACvBpD,EAAa,KAAK,CAChB,KAAM,eACN,SAAU,UACV,SAAU,YACV,KAAME,EACN,QAAS,iBAAiBkD,CAAQ,yBAClC,KAAM,8JACR,CAAC,MACI,CAIL,IAAM9E,EAASJ,EAAYkF,CAAQ,GAC/B9E,GAAA,YAAAA,EAAQ,UAAW,SAAWA,EAAO,GAAK,GAAKA,EAAO,GAAK,IAC7D0B,EAAa,KAAK,CAChB,KAAM,wBACN,SAAU,UACV,SAAU,YACV,KAAME,EACN,QAAS,gBAAgBkD,CAAQ,+HACjC,KAAM,iKACR,CAAC,CAEL,CAKF,IAAMC,EAAcvB,EAAQ,YAC5B,GAAI,OAAOuB,GAAgB,UAAYA,IAAgB,UAAW,CAChE,IAAM/E,EAASJ,EAAYmF,CAAW,EAClC,CAAC/E,GAAUA,EAAO,SAAW,QAC/B0B,EAAa,KAAK,CAChB,KAAM,kBACN,SAAU,UACV,SAAU,YACV,KAAME,EACN,QAAS,oBAAoBmD,CAAW,mCACxC,KAAM,sGACR,CAAC,EACQ/E,EAAO,EAAI,GACpB0B,EAAa,KAAK,CAChB,KAAM,kBACN,SAAU,QACV,SAAU,YACV,KAAME,EACN,QAAS,oBAAoBmD,CAAW,OAAO/E,EAAO,CAAC,yEACvD,KAAM,6FACR,CAAC,CAEL,CAIA,IAAMgF,EAAWxB,EAAQ,SACzB,GAAI,OAAOwB,GAAa,UAAYA,IAAa,UAAW,CAC1D,IAAMhF,EAASJ,EAAYoF,CAAQ,EAC/B,CAAChF,GAAUA,EAAO,SAAW,QAC/B0B,EAAa,KAAK,CAChB,KAAM,eACN,SAAU,UACV,SAAU,YACV,KAAME,EACN,QAAS,iBAAiBoD,CAAQ,gCAClC,KAAM,mGACR,CAAC,EACQhF,EAAO,EAAI,GACpB0B,EAAa,KAAK,CAChB,KAAM,eACN,SAAU,QACV,SAAU,YACV,KAAME,EACN,QAAS,iBAAiBoD,CAAQ,OAAOhF,EAAO,CAAC,sEACjD,KAAM,oDACR,CAAC,CAEL,CAGA,GAAIkC,EAAQ,OAASA,EAAQ,MAAM,OAAS,EAC1C,QAAW+C,KAAQ/C,EAAQ,MAAO,CAChC,IAAIgD,EACJ,GAAI,CACFA,EAAaD,EAAK,MAAMzB,EAAS5B,EAAM6B,CAAG,CAC5C,OAAQlD,EAAA,CACN,QACF,CACA,QAAWmE,KAAKQ,EACdxD,EAAa,KAAK,CAChB,KAAMuD,EAAK,GACX,UAAUtC,EAAA+B,EAAE,WAAF,KAAA/B,EAAcsC,EAAK,SAC7B,SAAUA,EAAK,SACf,KAAMrD,EACN,QAAS8C,EAAE,QACX,KAAMA,EAAE,IACV,CAAC,CAEL,CAMF,IAAM/C,EAA6B,CAAC,EACpCQ,EAAKwB,EAAS/B,EAAMD,EAAc,GAAOO,CAAO,EAGhDV,EAAagC,EAAQ,eAAgB9B,EAAcC,EAAcC,EAAMC,CAAG,CAC5E,CAyBO,SAASsD,EACdlD,EACAC,EAA2B,CAAC,EACV,CAClB,IAAMkD,EAASpD,EAASC,EAAMC,CAAO,EAC/BmD,EAA6B,CACjC,MAAO,EACP,QAAS,EACT,KAAM,EACN,MAAOD,EAAO,MAChB,EACA,QAAWE,KAASF,EAAQC,EAAQC,EAAM,QAAQ,GAAK,EACvD,MAAO,CAAE,GAAID,EAAQ,QAAU,EAAG,OAAAD,EAAQ,QAAAC,CAAQ,CACpD,CAGO,SAASE,EAAOC,EAAmC,CACxD,GAAIA,EAAY,SAAW,EAAG,MAAO,0BACrC,IAAMC,EAAQC,GACZA,IAAM,QAAU,SAAMA,IAAM,UAAY,SAAM,IAChD,OAAOF,EACJ,IACE1D,GACC,GAAG2D,EAAK3D,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,CEr1BA,SAAS6D,EAAUC,EAAyB,CAC1C,GAAI,MAAM,QAAQA,CAAK,EAAG,OAAOA,EAAM,IAAID,CAAS,EACpD,GAAIE,EAAcD,CAAK,EAAG,CACxB,IAAME,EAA+B,CAAC,EACtC,QAAWC,KAAOH,EAAOE,EAAIC,CAAG,EAAIJ,EAAUC,EAAMG,CAAG,CAAC,EACxD,OAAOD,CACT,CACA,OAAOF,CACT,CA4BO,SAASI,EAAIC,EAAeC,EAA2B,CAAC,EAAc,CAC3E,IAAMC,EAAOR,EAAUM,CAAI,EACrBG,EAAwB,CAAC,EAC/B,OAAAC,EAAQF,EAAM,GAAIC,CAAO,EAClB,CAAE,KAAAD,EAAM,QAAAC,EAAS,OAAQE,EAASH,EAAMD,CAAO,CAAE,CAC1D,CAEA,SAASG,EAAQE,EAAeC,EAAcJ,EAA6B,CACzE,GAAI,MAAM,QAAQG,CAAI,EAAG,CACvB,OAAW,CAACE,EAAOC,CAAK,IAAKH,EAAK,QAAQ,EACxCF,EAAQK,EAAO,GAAGF,CAAI,IAAIC,CAAK,IAAKL,CAAO,EAE7C,MACF,CACA,GAAI,CAACP,EAAcU,CAAI,EAAG,OAE1B,IAAMI,EAAMC,EAAQL,CAAI,EACxB,GAAI,CAACI,EAAK,OACV,IAAME,EAAOL,EAAO,GAAGA,CAAI,MAAMG,CAAG,GAAKA,EAIrCG,EAAK,IAAIH,CAAG,GAAKJ,EAAKI,CAAG,IAAM,MAAQJ,EAAKI,CAAG,IAAM,SACvDJ,EAAKI,CAAG,EAAI,KACZP,EAAQ,KAAK,CACX,KAAM,eACN,KAAMS,EACN,QAAS,aAAaF,CAAG,+CAC3B,CAAC,GAGHN,EAAQE,EAAKI,CAAG,EAAGE,EAAMT,CAAO,CAClC","names":["src_exports","__export","diagnose","fix","format","validate","__toCommonJS","import_palette","import_core","TAGS","VOID","isPlainObject","value","findTag","element","key","RESERVED","TYPOGRAPHY_STYLE","COLOR_STYLE","DIRECT_COLOR_PROPS","CSS_SEMANTIC_VALUES","LITERAL_COLOR","SPACING_STYLE","LITERAL_SPACING","parseOffset","value","m","isValidTone","parsed","parseLiteralToLch","trimmed","rgb","hex","lab","lch","e","EMBEDDED_COLOR","extractColorLiteral","match","buildColorHint","L","C","h","rawOffset","offset","toneStr","colorFamily","buildSpacingHint","prop","amount","unit","n","applyDisable","disable","elementDiags","contentDiags","here","out","d","disabled","diagnose","root","options","walk","only","exclude","node","path","dynamic","_a","_b","_c","runReactive","result","elementItems","child","isPlainObject","findTag","item","index","seenKeys","key","literalKey","count","element","tag","contentKeys","content","VOID","style","colorLiteral","colorHint","spacingHint","bgProp","bgResult","bgMatch","styleProp","colorFn","bgFn","colorVar","bgVar","extractShift","v","textShift","bgShift","diff","dataTone","dataDensity","dataSize","rule","violations","validate","issues","summary","issue","format","diagnostics","icon","s","cloneTree","value","isPlainObject","out","key","fix","root","options","tree","applied","walkFix","validate","node","path","index","child","tag","findTag","here","VOID"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,14 +1,65 @@
|
|
|
1
1
|
type Severity = "error" | "warning" | "info";
|
|
2
|
+
/**
|
|
3
|
+
* Broad structural category for a rule. Mirrors Biome's lint category model.
|
|
4
|
+
* Built-in rules always set this; custom rules may omit it.
|
|
5
|
+
*/
|
|
6
|
+
type RuleCategory = "structure" | "key" | "theme" | "typography" | "data-attr";
|
|
2
7
|
interface Diagnostic {
|
|
3
8
|
/** Rule id, e.g. "inline-typography". */
|
|
4
9
|
rule: string;
|
|
5
10
|
severity: Severity;
|
|
11
|
+
/**
|
|
12
|
+
* Broad structural category. Built-in rules always set this.
|
|
13
|
+
* Custom rules may omit it.
|
|
14
|
+
*/
|
|
15
|
+
category?: RuleCategory;
|
|
6
16
|
/** Human path to the offending node, e.g. "div > ul > li". */
|
|
7
17
|
path: string;
|
|
8
18
|
message: string;
|
|
9
19
|
/** How to fix it. */
|
|
10
20
|
hint?: string;
|
|
11
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A custom rule that extends the doctor with project-specific checks.
|
|
24
|
+
* Custom rules run alongside the 12 built-in rules; their ids must not
|
|
25
|
+
* clash with any built-in id.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const noEmptyContent: CustomRule = {
|
|
30
|
+
* id: "no-empty-content",
|
|
31
|
+
* severity: "warning",
|
|
32
|
+
* category: "structure",
|
|
33
|
+
* check: (element, path, tag) => {
|
|
34
|
+
* if (element[tag] === "") {
|
|
35
|
+
* return [{ message: `Empty string on <${tag}> — use null or text.` }]
|
|
36
|
+
* }
|
|
37
|
+
* return []
|
|
38
|
+
* },
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* diagnose(tree, { rules: [noEmptyContent] })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
interface CustomRule {
|
|
45
|
+
/** Unique id shown in diagnostics. Must not clash with any built-in rule id. */
|
|
46
|
+
id: string;
|
|
47
|
+
/** Default severity for violations produced by this rule. */
|
|
48
|
+
severity: Severity;
|
|
49
|
+
/** Category for display and filtering. Optional. */
|
|
50
|
+
category?: RuleCategory;
|
|
51
|
+
/**
|
|
52
|
+
* Called once per element node (nodes that have a valid HTML/SVG tag).
|
|
53
|
+
* Return an array of violation descriptors. The engine fills in `rule`,
|
|
54
|
+
* `severity`, `category`, and `path`; provide `message` and optionally
|
|
55
|
+
* `hint`. Pass `severity` in the descriptor to override the rule default.
|
|
56
|
+
*/
|
|
57
|
+
check: (element: Record<string, unknown>, path: string, tag: string) => Array<{
|
|
58
|
+
message: string;
|
|
59
|
+
hint?: string;
|
|
60
|
+
severity?: Severity;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
12
63
|
interface DiagnoseOptions {
|
|
13
64
|
/**
|
|
14
65
|
* Invoke reactive content functions `(listener) => …` with a no-op listener to
|
|
@@ -16,6 +67,23 @@ interface DiagnoseOptions {
|
|
|
16
67
|
* Set false if your reactive functions have side effects.
|
|
17
68
|
*/
|
|
18
69
|
runReactive?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* If set, only emit diagnostics whose rule id is in this list.
|
|
72
|
+
* Takes precedence over `exclude`.
|
|
73
|
+
* Applies to both built-in and custom rules.
|
|
74
|
+
*/
|
|
75
|
+
only?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Rule ids to skip entirely.
|
|
78
|
+
* Ignored when `only` is also set.
|
|
79
|
+
* Applies to both built-in and custom rules.
|
|
80
|
+
*/
|
|
81
|
+
exclude?: string[];
|
|
82
|
+
/**
|
|
83
|
+
* Additional custom rules to run alongside the 12 built-in rules.
|
|
84
|
+
* Custom rule ids are also subject to `only`/`exclude` filtering.
|
|
85
|
+
*/
|
|
86
|
+
rules?: CustomRule[];
|
|
19
87
|
}
|
|
20
88
|
/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */
|
|
21
89
|
declare function diagnose(root: unknown, options?: DiagnoseOptions): Diagnostic[];
|
|
@@ -70,4 +138,4 @@ interface FixResult {
|
|
|
70
138
|
*/
|
|
71
139
|
declare function fix(root: unknown, options?: DiagnoseOptions): FixResult;
|
|
72
140
|
|
|
73
|
-
export { type AppliedFix, type DiagnoseOptions, type Diagnostic, type FixResult, type Severity, type ValidationReport, type ValidationSummary, diagnose, fix, format, validate };
|
|
141
|
+
export { type AppliedFix, type CustomRule, type DiagnoseOptions, type Diagnostic, type FixResult, type RuleCategory, type Severity, type ValidationReport, type ValidationSummary, diagnose, fix, format, validate };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,65 @@
|
|
|
1
1
|
type Severity = "error" | "warning" | "info";
|
|
2
|
+
/**
|
|
3
|
+
* Broad structural category for a rule. Mirrors Biome's lint category model.
|
|
4
|
+
* Built-in rules always set this; custom rules may omit it.
|
|
5
|
+
*/
|
|
6
|
+
type RuleCategory = "structure" | "key" | "theme" | "typography" | "data-attr";
|
|
2
7
|
interface Diagnostic {
|
|
3
8
|
/** Rule id, e.g. "inline-typography". */
|
|
4
9
|
rule: string;
|
|
5
10
|
severity: Severity;
|
|
11
|
+
/**
|
|
12
|
+
* Broad structural category. Built-in rules always set this.
|
|
13
|
+
* Custom rules may omit it.
|
|
14
|
+
*/
|
|
15
|
+
category?: RuleCategory;
|
|
6
16
|
/** Human path to the offending node, e.g. "div > ul > li". */
|
|
7
17
|
path: string;
|
|
8
18
|
message: string;
|
|
9
19
|
/** How to fix it. */
|
|
10
20
|
hint?: string;
|
|
11
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A custom rule that extends the doctor with project-specific checks.
|
|
24
|
+
* Custom rules run alongside the 12 built-in rules; their ids must not
|
|
25
|
+
* clash with any built-in id.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const noEmptyContent: CustomRule = {
|
|
30
|
+
* id: "no-empty-content",
|
|
31
|
+
* severity: "warning",
|
|
32
|
+
* category: "structure",
|
|
33
|
+
* check: (element, path, tag) => {
|
|
34
|
+
* if (element[tag] === "") {
|
|
35
|
+
* return [{ message: `Empty string on <${tag}> — use null or text.` }]
|
|
36
|
+
* }
|
|
37
|
+
* return []
|
|
38
|
+
* },
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* diagnose(tree, { rules: [noEmptyContent] })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
interface CustomRule {
|
|
45
|
+
/** Unique id shown in diagnostics. Must not clash with any built-in rule id. */
|
|
46
|
+
id: string;
|
|
47
|
+
/** Default severity for violations produced by this rule. */
|
|
48
|
+
severity: Severity;
|
|
49
|
+
/** Category for display and filtering. Optional. */
|
|
50
|
+
category?: RuleCategory;
|
|
51
|
+
/**
|
|
52
|
+
* Called once per element node (nodes that have a valid HTML/SVG tag).
|
|
53
|
+
* Return an array of violation descriptors. The engine fills in `rule`,
|
|
54
|
+
* `severity`, `category`, and `path`; provide `message` and optionally
|
|
55
|
+
* `hint`. Pass `severity` in the descriptor to override the rule default.
|
|
56
|
+
*/
|
|
57
|
+
check: (element: Record<string, unknown>, path: string, tag: string) => Array<{
|
|
58
|
+
message: string;
|
|
59
|
+
hint?: string;
|
|
60
|
+
severity?: Severity;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
12
63
|
interface DiagnoseOptions {
|
|
13
64
|
/**
|
|
14
65
|
* Invoke reactive content functions `(listener) => …` with a no-op listener to
|
|
@@ -16,6 +67,23 @@ interface DiagnoseOptions {
|
|
|
16
67
|
* Set false if your reactive functions have side effects.
|
|
17
68
|
*/
|
|
18
69
|
runReactive?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* If set, only emit diagnostics whose rule id is in this list.
|
|
72
|
+
* Takes precedence over `exclude`.
|
|
73
|
+
* Applies to both built-in and custom rules.
|
|
74
|
+
*/
|
|
75
|
+
only?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Rule ids to skip entirely.
|
|
78
|
+
* Ignored when `only` is also set.
|
|
79
|
+
* Applies to both built-in and custom rules.
|
|
80
|
+
*/
|
|
81
|
+
exclude?: string[];
|
|
82
|
+
/**
|
|
83
|
+
* Additional custom rules to run alongside the 12 built-in rules.
|
|
84
|
+
* Custom rule ids are also subject to `only`/`exclude` filtering.
|
|
85
|
+
*/
|
|
86
|
+
rules?: CustomRule[];
|
|
19
87
|
}
|
|
20
88
|
/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */
|
|
21
89
|
declare function diagnose(root: unknown, options?: DiagnoseOptions): Diagnostic[];
|
|
@@ -70,4 +138,4 @@ interface FixResult {
|
|
|
70
138
|
*/
|
|
71
139
|
declare function fix(root: unknown, options?: DiagnoseOptions): FixResult;
|
|
72
140
|
|
|
73
|
-
export { type AppliedFix, type DiagnoseOptions, type Diagnostic, type FixResult, type Severity, type ValidationReport, type ValidationSummary, diagnose, fix, format, validate };
|
|
141
|
+
export { type AppliedFix, type CustomRule, type DiagnoseOptions, type Diagnostic, type FixResult, type RuleCategory, type Severity, type ValidationReport, type ValidationSummary, diagnose, fix, format, validate };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{cssRgbToRgb as I,hexToRgb as V,labToLch as C,rgbToLab as E}from"@domphy/palette";import{HtmlTags as O,SvgTags as L,VoidTags as N}from"@domphy/core";var _=new Set([...O,...L]),$=new Set(N);function h(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function b(n){for(let t in n)if(_.has(t))return t}var F=new Set(["$","style","_key","_portal","_context","_metadata"]),M=new Set(["fontSize","lineHeight","fontWeight","letterSpacing","fontFamily","textDecoration"]),z=new Set(["color","backgroundColor","background","borderColor","border","outlineColor","outline","fill","stroke"]),P=/#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/,B=new Set(["margin","marginTop","marginRight","marginBottom","marginLeft","marginInline","marginBlock","marginInlineStart","marginInlineEnd","marginBlockStart","marginBlockEnd","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingInline","paddingBlock","paddingInlineStart","paddingInlineEnd","paddingBlockStart","paddingBlockEnd","gap","rowGap","columnGap"]),H=/^(\d+(?:\.\d+)?)(rem|em|px)$/;function k(n){let t=n.match(/^(increase|decrease|shift)-(\d+)$/);return t?{family:t[1],n:parseInt(t[2],10)}:null}function j(n){if(n==="inherit"||n==="base"||/^-?\d+$/.test(n))return!0;let t=k(n);return t?t.n<=17:!1}function G(n){try{let t=n.trim(),e;if(t.startsWith("#")){let i=t;if(i.length===9&&(i=i.slice(0,7)),i.length===5&&(i=i.slice(0,4)),i.length===4&&(i=`#${i[1]}${i[1]}${i[2]}${i[2]}${i[3]}${i[3]}`),i.length!==7)return null;e=V(i)}else if(/^rgba?\s*\(/.test(t))e=I(t);else return null;let r=E(e),c=C(r);return[c[0],c[1],c[2]]}catch(t){return null}}var W=/#[0-9a-fA-F]{3,8}\b|rgba?\s*\([^)]*\)/;function U(n){let t=W.exec(n);return t?t[0]:null}function K(n){let[t,e,r]=n,c=Math.round((t-50)/10),i=Math.max(-9,Math.min(9,c)),u;Math.abs(i)<=1?u='"base"':i<0?u=`"decrease-${Math.abs(i)}"`:u=`"increase-${i}"`;let o;return e<12?o="neutral":r<30||r>=330?o="error":r<75?o="warning":r<165?o="success":(r<265,o="primary"),`(l) => themeColor(l, ${u}, "${o}") [perceptual LCH L=${Math.round(t)} C=${Math.round(e)} h=${Math.round(r)}\xB0]`}function Y(n,t){let e=H.exec(t);if(!e)return null;let r=parseFloat(e[1]),c=e[2],i;return c==="rem"||c==="em"?i=Math.round(r*4):i=Math.round(r/4),i<=0?null:`${n}: themeSpacing(${i}) \u2014 themeSpacing(n)=n/4em, so ${i}/4=${i/4}em \u2248 ${t} at default density`}function A(n,t={}){let e=[];return w(n,"",e,!1,t.runReactive!==!1),e}function w(n,t,e,r,c){var D,T;if(typeof n=="function"){if(!c)return;let s;try{s=n(()=>{})}catch(l){return}w(s,t,e,!0,c);return}if(Array.isArray(n)){let s=n.filter(a=>h(a)&&b(a));r&&(s.length>1&&s.some(a=>a._key===void 0)&&e.push({rule:"missing-key",severity:"warning",path:t||"(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."}),s.length>1&&s.every((a,f)=>a._key===f)&&e.push({rule:"unstable-key",severity:"warning",path:t||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let l=new Map;for(let a of s){let f=a._key;if(f==null)continue;let g=`${typeof f}:${String(f)}`;l.set(g,((D=l.get(g))!=null?D:0)+1)}for(let[a,f]of l)if(f>1){let g=a.slice(a.indexOf(":")+1);e.push({rule:"duplicate-key",severity:"error",path:t||"(list)",message:`Duplicate \`_key\` "${g}" among ${f} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}n.forEach((a,f)=>{w(a,`${t}[${f}]`,e,!1,c)});return}if(!h(n))return;let i=n,u=b(i),o=u?t?`${t} > ${u}`:u:t||"(root)";if(!u){let s=Object.keys(i).filter(l=>!F.has(l)&&!l.startsWith("_on")&&!l.startsWith("on")&&!l.startsWith("data")&&!l.startsWith("aria"));s.length===1&&e.push({rule:"unknown-tag",severity:"warning",path:o,message:`"${s[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 d=i[u];if($.has(u)&&d!==null&&d!==void 0&&e.push({rule:"void-content",severity:"error",path:o,message:`Void tag "${u}" must have null content (got ${Array.isArray(d)?"array":typeof d}).`,hint:`Write { ${u}: null, \u2026 } and put attributes as sibling keys.`}),h(i.style)){let s=i.style;for(let l in s){let a=s[l];if(M.has(l)&&typeof a!="function"&&e.push({rule:"inline-typography",severity:"warning",path:o,message:`Inline \`${l}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."}),z.has(l)&&typeof a=="string"&&P.test(a)){let f=(T=U(a))!=null?T:a,g=G(f),R=g?K(g):"(l) => themeColor(l, tone, colorName)";e.push({rule:"raw-theme-value",severity:"info",path:o,message:`Inline \`${l}\` uses a literal color (${a}).`,hint:`Prefer a theme token \u2014 ${R} \u2014 so theming and dark mode apply.`})}if(B.has(l)&&typeof a=="string"){let f=Y(l,a);f&&e.push({rule:"raw-spacing-value",severity:"info",path:o,message:`Inline \`${l}: "${a}"\` uses a literal spacing value.`,hint:`Prefer themeSpacing() for theme density: ${f}`})}}}let p=i.dataTone;if(typeof p=="string")if(!j(p))e.push({rule:"unknown-tone",severity:"warning",path:o,message:`\`dataTone\` "${p}" is not a valid tone.`,hint:'Use "inherit", "base", a number, or "shift-N"/"increase-N"/"decrease-N" with N \u2264 17 (the ramp has 18 steps). Words like "surface"/"text" are not tones.'});else{let s=k(p);(s==null?void 0:s.family)==="shift"&&s.n>=4&&s.n<=13&&e.push({rule:"middle-surface-anchor",severity:"warning",path:o,message:`\`dataTone: "${p}"\` uses a mid-ramp surface anchor (steps 4\u201313). Child tones derived from this surface may clamp and collapse contrast.`,hint:"Prefer edge anchors: shift-0\u20133 for light surfaces, shift-14\u201317 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions."})}let m=i.dataDensity;if(typeof m=="string"&&m!=="inherit"){let s=k(m);!s||s.family==="shift"?e.push({rule:"unknown-density",severity:"warning",path:o,message:`\`dataDensity\` "${m}" is not a valid density offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20134. "shift-" is not valid for density.'}):s.n>4&&e.push({rule:"unknown-density",severity:"error",path:o,message:`\`dataDensity\` "${m}" N=${s.n} is out of range \u2014 the density scale has 5 steps (max offset: 4).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 4. Density factors: [0.75, 1, 1.5, 2, 2.5].'})}let y=i.dataSize;if(typeof y=="string"&&y!=="inherit"){let s=k(y);!s||s.family==="shift"?e.push({rule:"unknown-size",severity:"warning",path:o,message:`\`dataSize\` "${y}" is not a valid size offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20137. "shift-" is not valid for size.'}):s.n>7&&e.push({rule:"unknown-size",severity:"error",path:o,message:`\`dataSize\` "${y}" N=${s.n} is out of range \u2014 the size scale has 8 steps (max offset: 7).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 7.'})}w(d,o,e,!1,c)}function x(n,t={}){let e=A(n,t),r={error:0,warning:0,info:0,total:e.length};for(let c of e)r[c.severity]+=1;return{ok:r.error===0,issues:e,summary:r}}function q(n){if(n.length===0)return"\u2713 No issues found.";let t=e=>e==="error"?"\u2717":e==="warning"?"\u26A0":"i";return n.map(e=>`${t(e.severity)} [${e.rule}] ${e.path}
|
|
2
|
-
${
|
|
3
|
-
\u2192 ${
|
|
4
|
-
`)}function
|
|
1
|
+
import{cssRgbToRgb as j,hexToRgb as G,labToLch as U,rgbToLab as K}from"@domphy/palette";import{HtmlTags as z,SvgTags as B,VoidTags as H}from"@domphy/core";var W=new Set([...z,...B]),v=new Set(H);function y(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function $(e){for(let t in e)if(W.has(t))return t}var Y=new Set(["$","style","_key","_portal","_context","_metadata","_doctorDisable"]),q=new Set(["fontSize","lineHeight","fontWeight","letterSpacing","fontFamily","textDecoration"]),J=new Set(["color","backgroundColor","background","borderColor","border","outlineColor","outline","fill","stroke"]),Q=new Set(["color","fill","stroke","backgroundColor","outlineColor","borderColor","caretColor","accentColor","columnRuleColor","textDecorationColor"]),X=new Set(["transparent","currentcolor","inherit","initial","unset","none","auto","revert","revert-layer",""]),M=/#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/,Z=new Set(["margin","marginTop","marginRight","marginBottom","marginLeft","marginInline","marginBlock","marginInlineStart","marginInlineEnd","marginBlockStart","marginBlockEnd","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingInline","paddingBlock","paddingInlineStart","paddingInlineEnd","paddingBlockStart","paddingBlockEnd","gap","rowGap","columnGap"]),ee=/^(\d+(?:\.\d+)?)(rem|em|px)$/;function S(e){let t=e.match(/^(increase|decrease|shift)-(\d+)$/);return t?{family:t[1],n:parseInt(t[2],10)}:null}function te(e){if(e==="inherit"||e==="base"||/^-?\d+$/.test(e))return!0;let t=S(e);return t?t.n<=17:!1}function ne(e){try{let t=e.trim(),n;if(t.startsWith("#")){let i=t;if(i.length===9&&(i=i.slice(0,7)),i.length===5&&(i=i.slice(0,4)),i.length===4&&(i=`#${i[1]}${i[1]}${i[2]}${i[2]}${i[3]}${i[3]}`),i.length!==7)return null;n=G(i)}else if(/^rgba?\s*\(/.test(t))n=j(t);else return null;let a=K(n),o=U(a);return[o[0],o[1],o[2]]}catch(t){return null}}var re=/#[0-9a-fA-F]{3,8}\b|rgba?\s*\([^)]*\)/;function ie(e){let t=re.exec(e);return t?t[0]:null}function se(e){let[t,n,a]=e,o=Math.round((t-50)/10),i=Math.max(-9,Math.min(9,o)),c;Math.abs(i)<=1?c='"base"':i<0?c=`"decrease-${Math.abs(i)}"`:c=`"increase-${i}"`;let g;return n<12?g="neutral":a<30||a>=330?g="error":a<75?g="warning":a<165?g="success":(a<265,g="primary"),`(l) => themeColor(l, ${c}, "${g}") [perceptual LCH L=${Math.round(t)} C=${Math.round(n)} h=${Math.round(a)}\xB0]`}function ae(e,t){let n=ee.exec(t);if(!n)return null;let a=parseFloat(n[1]),o=n[2],i;return o==="rem"||o==="em"?i=Math.round(a*4):i=Math.round(a/4),i<=0?null:`${e}: themeSpacing(${i}) \u2014 themeSpacing(n)=n/4em, so ${i}/4=${i/4}em \u2248 ${t} at default density`}function F(e,t,n,a,o){if(e===!0){for(let i of n)i.path!==a&&o.push(i);return}if(e!=null&&e!==!1){let i=new Set(Array.isArray(e)?e:[String(e)]);for(let c of t)i.has(c.rule)||o.push(c);for(let c of n)c.path===a&&i.has(c.rule)||o.push(c);return}o.push(...t),o.push(...n)}function P(e,t={}){let n=[];if(R(e,"",n,!1,t),t.only!==void 0){if(t.only.length===0)return[];let a=new Set(t.only);return n.filter(o=>a.has(o.rule))}if(t.exclude&&t.exclude.length>0){let a=new Set(t.exclude);return n.filter(o=>!a.has(o.rule))}return n}function R(e,t,n,a,o){var I,N,V;let i=o.runReactive!==!1;if(typeof e=="function"){if(!i)return;let r;try{r=e(()=>{})}catch(l){return}R(r,t,n,!0,o);return}if(Array.isArray(e)){let r=e.filter(s=>y(s)&&$(s));a&&(r.length>1&&r.some(s=>s._key===void 0)&&n.push({rule:"missing-key",severity:"warning",category:"key",path:t||"(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."}),r.length>1&&r.every((s,f)=>s._key===f)&&n.push({rule:"unstable-key",severity:"warning",category:"key",path:t||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let l=new Map;for(let s of r){let f=s._key;if(f==null)continue;let d=`${typeof f}:${String(f)}`;l.set(d,((I=l.get(d))!=null?I:0)+1)}for(let[s,f]of l)if(f>1){let d=s.slice(s.indexOf(":")+1);n.push({rule:"duplicate-key",severity:"error",category:"key",path:t||"(list)",message:`Duplicate \`_key\` "${d}" among ${f} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}e.forEach((s,f)=>{R(s,`${t}[${f}]`,n,!1,o)});return}if(!y(e))return;let c=e,g=$(c),u=g?t?`${t} > ${g}`:g:t||"(root)",h=[];if(!g){let r=Object.keys(c).filter(l=>!Y.has(l)&&!l.startsWith("_on")&&!l.startsWith("on")&&!l.startsWith("data")&&!l.startsWith("aria"));r.length===1&&h.push({rule:"unknown-tag",severity:"warning",category:"structure",path:u,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)."}),F(c._doctorDisable,h,[],u,n);return}let m=c[g];if(v.has(g)&&m!==null&&m!==void 0&&h.push({rule:"void-content",severity:"error",category:"structure",path:u,message:`Void tag "${g}" must have null content (got ${Array.isArray(m)?"array":typeof m}).`,hint:`Write { ${g}: null, \u2026 } and put attributes as sibling keys.`}),y(c.style)){let r=c.style;for(let l in r){let s=r[l];if(q.has(l)&&typeof s!="function"&&h.push({rule:"inline-typography",severity:"warning",category:"typography",path:u,message:`Inline \`${l}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."}),J.has(l)&&typeof s=="string"&&M.test(s)){let f=(N=ie(s))!=null?N:s,d=ne(f),x=d?se(d):"(l) => themeColor(l, tone, colorName)";h.push({rule:"raw-theme-value",severity:"info",category:"theme",path:u,message:`Inline \`${l}\` uses a literal color (${s}).`,hint:`Prefer a theme token \u2014 ${x} \u2014 so theming and dark mode apply.`})}if(Q.has(l)&&typeof s=="string"&&!M.test(s)&&!s.includes("(")&&!s.startsWith("--")&&!X.has(s.trim().toLowerCase())&&h.push({rule:"raw-theme-value",severity:"info",category:"theme",path:u,message:`Inline \`${l}\` uses a CSS named color ("${s}").`,hint:`CSS named colors like "${s}" bypass theming and dark mode. Prefer (l) => themeColor(l, tone, colorName) \u2014 so the theme context applies.`}),Z.has(l)&&typeof s=="string"){let f=ae(l,s);f&&h.push({rule:"raw-spacing-value",severity:"info",category:"theme",path:u,message:`Inline \`${l}: "${s}"\` uses a literal spacing value.`,hint:`Prefer themeSpacing() for theme density: ${f}`})}}}let O=y(c.style)?c.style.backgroundColor:void 0;if(typeof O=="function"&&i){let r;try{r=O(()=>{})}catch(l){}if(typeof r=="string"){let l=r.match(/var\(--[\w-]+-(\d+)\)$/);l&&parseInt(l[1],10)>0&&h.push({rule:"tone-background-inherit",severity:"warning",category:"theme",path:u,message:`\`style.backgroundColor\` uses a fixed tone (resolves to "${r}" at base context) instead of "inherit".`,hint:'backgroundColor should always be (l) => themeColor(l, "inherit"). To shift the surface tone, set dataTone on the container \u2014 it applies to all children uniformly.'})}}{let r=y(c.style)?c.style:null,l=r==null?void 0:r.color,s=r==null?void 0:r.backgroundColor;if(i&&typeof l=="function"&&typeof s=="function"){let f,d;try{f=l(()=>{}),d=s(()=>{})}catch(p){}let x=p=>{if(typeof p!="string")return null;let E=p.match(/var\(--[\w-]+-(\d+)\)$/);return E?parseInt(E[1],10):null},C=x(f),D=x(d);if(C!==null&&D!==null){let p=Math.abs(C-D);p<9&&h.push({rule:"low-contrast",severity:"warning",category:"theme",path:u,message:`Text/background shift gap is ${p} (shift-${C} vs shift-${D}) \u2014 contrast may be insufficient.`,hint:"Aim for \u22659 shift steps between text and surface. E.g. shift-0 bg + shift-9 text, or shift-11 text on a shift-0 surface. Increase the gap or rely on a parent dataTone to open it."})}}}let k=c.dataTone;if(typeof k=="string")if(!te(k))h.push({rule:"unknown-tone",severity:"warning",category:"data-attr",path:u,message:`\`dataTone\` "${k}" is not a valid tone.`,hint:'Use "inherit", "base", a number, or "shift-N"/"increase-N"/"decrease-N" with N \u2264 17 (the ramp has 18 steps). Words like "surface"/"text" are not tones.'});else{let r=S(k);(r==null?void 0:r.family)==="shift"&&r.n>=4&&r.n<=13&&h.push({rule:"middle-surface-anchor",severity:"warning",category:"data-attr",path:u,message:`\`dataTone: "${k}"\` uses a mid-ramp surface anchor (steps 4\u201313). Child tones derived from this surface may clamp and collapse contrast.`,hint:"Prefer edge anchors: shift-0\u20133 for light surfaces, shift-14\u201317 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions."})}let b=c.dataDensity;if(typeof b=="string"&&b!=="inherit"){let r=S(b);!r||r.family==="shift"?h.push({rule:"unknown-density",severity:"warning",category:"data-attr",path:u,message:`\`dataDensity\` "${b}" is not a valid density offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20134. "shift-" is not valid for density.'}):r.n>4&&h.push({rule:"unknown-density",severity:"error",category:"data-attr",path:u,message:`\`dataDensity\` "${b}" N=${r.n} is out of range \u2014 the density scale has 5 steps (max offset: 4).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 4. Density factors: [0.75, 1, 1.5, 2, 2.5].'})}let w=c.dataSize;if(typeof w=="string"&&w!=="inherit"){let r=S(w);!r||r.family==="shift"?h.push({rule:"unknown-size",severity:"warning",category:"data-attr",path:u,message:`\`dataSize\` "${w}" is not a valid size offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20137. "shift-" is not valid for size.'}):r.n>7&&h.push({rule:"unknown-size",severity:"error",category:"data-attr",path:u,message:`\`dataSize\` "${w}" N=${r.n} is out of range \u2014 the size scale has 8 steps (max offset: 7).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 7.'})}if(o.rules&&o.rules.length>0)for(let r of o.rules){let l;try{l=r.check(c,u,g)}catch(s){continue}for(let s of l)h.push({rule:r.id,severity:(V=s.severity)!=null?V:r.severity,category:r.category,path:u,message:s.message,hint:s.hint})}let L=[];R(m,u,L,!1,o),F(c._doctorDisable,h,L,u,n)}function T(e,t={}){let n=P(e,t),a={error:0,warning:0,info:0,total:n.length};for(let o of n)a[o.severity]+=1;return{ok:a.error===0,issues:n,summary:a}}function oe(e){if(e.length===0)return"\u2713 No issues found.";let t=n=>n==="error"?"\u2717":n==="warning"?"\u26A0":"i";return e.map(n=>`${t(n.severity)} [${n.rule}] ${n.path}
|
|
2
|
+
${n.message}${n.hint?`
|
|
3
|
+
\u2192 ${n.hint}`:""}`).join(`
|
|
4
|
+
`)}function A(e){if(Array.isArray(e))return e.map(A);if(y(e)){let t={};for(let n in e)t[n]=A(e[n]);return t}return e}function le(e,t={}){let n=A(e),a=[];return _(n,"",a),{tree:n,applied:a,report:T(n,t)}}function _(e,t,n){if(Array.isArray(e)){for(let[i,c]of e.entries())_(c,`${t}[${i}]`,n);return}if(!y(e))return;let a=$(e);if(!a)return;let o=t?`${t} > ${a}`:a;v.has(a)&&e[a]!==null&&e[a]!==void 0&&(e[a]=null,n.push({rule:"void-content",path:o,message:`Void tag <${a}> cannot have content \u2014 cleared to null.`})),_(e[a],o,n)}export{P as diagnose,le as fix,oe as format,T as validate};
|
|
5
5
|
//# sourceMappingURL=index.js.map
|