export type RawHtmlPrecisionTier = | "exact-html" | "html-advisory" | "input-type" | "role-reimpl" | "interactive-nonsemantic" | "icon-only"; export interface RawHtmlCanonicalMatch { canonical: string; tier: RawHtmlPrecisionTier; } const EXACT_HTML_CANONICALS = new Map([["button", "Button"]]); const ADVISORY_HTML_CANONICALS = new Map([ ["textarea", "Textarea"], ["select", "Select"], ["dialog", "Dialog"], ["progress", "Progress"], ["details", "Collapsible"], ["table", "Table"], ]); const INPUT_TYPE_CANONICALS = new Map([ ["checkbox", "Checkbox"], ["radio", "Radio"], ["number", "NumberInput"], ["password", "PasswordInput"], ["range", "Slider"], ]); const ROLE_CANONICALS = new Map([ ["button", "Button"], ["checkbox", "Checkbox"], ["radio", "Radio"], ["switch", "Switch"], ["tab", "Tabs"], ["tablist", "Tabs"], ["dialog", "Dialog"], ["menu", "Menu"], ["status", "Alert"], ]); export const RAW_HTML_CANONICAL_TAGS = Object.freeze( Array.from(EXACT_HTML_CANONICALS, ([tagName, canonical]) => ({ tagName, canonical, tier: "exact-html" as const, })) ); export const RAW_HTML_ADVISORY_TAGS = Object.freeze( Array.from(ADVISORY_HTML_CANONICALS, ([tagName, canonical]) => ({ tagName, canonical, tier: "html-advisory" as const, })) ); export const RAW_HTML_INPUT_TYPE_CANONICALS = Object.freeze( Array.from(INPUT_TYPE_CANONICALS, ([type, canonical]) => ({ type, canonical, tier: "input-type" as const, })) ); export const RAW_HTML_ROLE_CANONICALS = Object.freeze( Array.from(ROLE_CANONICALS, ([role, canonical]) => ({ role, canonical, tier: "role-reimpl" as const, })) ); export function resolveCanonicalForRawHtml( tagName: string, inputType?: string ): RawHtmlCanonicalMatch | null { const tag = tagName.toLowerCase(); if (tag === "input") { const normalizedType = inputType?.trim().toLowerCase(); if (!normalizedType) return null; const canonical = INPUT_TYPE_CANONICALS.get(normalizedType); return canonical ? { canonical, tier: "input-type" } : null; } const exactCanonical = EXACT_HTML_CANONICALS.get(tag); if (exactCanonical) return { canonical: exactCanonical, tier: "exact-html" }; const advisoryCanonical = ADVISORY_HTML_CANONICALS.get(tag); return advisoryCanonical ? { canonical: advisoryCanonical, tier: "html-advisory" } : null; } export function resolveCanonicalForAriaRole(role: string): RawHtmlCanonicalMatch | null { const canonical = ROLE_CANONICALS.get(role.trim().toLowerCase()); return canonical ? { canonical, tier: "role-reimpl" } : null; } export function isRawHtmlAdvisoryTier(tier: RawHtmlPrecisionTier): boolean { return ( tier === "html-advisory" || tier === "role-reimpl" || tier === "interactive-nonsemantic" || tier === "icon-only" ); } /** * Low-frequency, typically-1:1 semantic tags where a per-component inferred * `htmlEquivalent` mapping is safe even with a custom-named component * (`` → Modal). Mirrors `ADVISORY_HTML_CANONICALS` + the `input` family. * * Deliberately EXCLUDES generic structural containers (`div`, `span`) and the * ambiguous common tags (`button`, `a`): those are handled by the stricter * agreement check in `isEnforceableHtmlEquivalent`. */ const SEMANTIC_HTML_EQUIVALENT_TAGS = new Set([ "input", "textarea", "select", "dialog", "progress", "details", "table", ]); /** * Specific interactive tags that an EXPLICITLY-DECLARED project mapping may * enforce against ANY canonical component NAME — making the rule component- * library agnostic (so `@acme/ui`'s `AcmeButton` fires on a raw ``, not * only the curated built-in "Button"). * * Deliberately EXCLUDES generic structural containers (`div`, `span`): even an * explicit declaration must never turn every `` into a bespoke * re-implementation. The `explicit` gate only applies to user-declared tag * mappings (provenance: `localCanonical.resolves`), never to inferred * `htmlEquivalent`s — so the `` → Chip flood class stays name-gated. */ const EXPLICITLY_ENFORCEABLE_AMBIGUOUS_TAGS = new Set(["button", "a"]); /** * Decide whether a per-component `htmlEquivalent` mapping (raw `tag` → a * `canonical` component) may drive a bare-tag canonical-usage finding. * * The contract: * - Low-frequency SEMANTIC tags (dialog, select, textarea, input, …) are always * enforceable, so a custom-named component (`` → Modal) still maps. * - Common/ambiguous tags (button, a, …) enforce ONLY when the mapping AGREES * with the framework's curated canonical for that tag — so `` → Button * fires, but `` → Chip does not. * - Generic structural containers (div, span) have no curated answer and are not * semantic → never enforce. * * This is the guard that stops the canonical-usage flood: a library component * being built on a `` must not make every `` a bespoke re-implementation * of it (every `` → Accordion, every `` → Badge, every `` → Chip). */ export function isEnforceableHtmlEquivalent(args: { tag: string; canonical: string; inputType?: string; /** * The mapping is an EXPLICIT project declaration of this tag → component * (provenance: `localCanonical.resolves`), not a low-confidence inferred * `htmlEquivalent`. Explicit declarations enforce a specific interactive tag * (`button`/`a`) against any canonical NAME — the agnosticism path — while * inferred mappings stay name-gated to the curated canonical (the flood * guard). Generic containers (`div`/`span`) are never enforceable even when * explicit. */ explicit?: boolean; }): boolean { const tag = args.tag.toLowerCase(); if (SEMANTIC_HTML_EQUIVALENT_TAGS.has(tag)) return true; if (args.explicit && EXPLICITLY_ENFORCEABLE_AMBIGUOUS_TAGS.has(tag)) return true; const builtIn = resolveCanonicalForRawHtml(tag, args.inputType); return builtIn !== null && builtIn.canonical === args.canonical; }