{"version":3,"file":"validateHTML.cjs","names":[],"sources":["../../../../src/transpiler/html/validateHTML.ts"],"sourcesContent":["export const VOID_HTML_ELEMENTS = new Set([\n  'area',\n  'base',\n  'br',\n  'col',\n  'embed',\n  'hr',\n  'img',\n  'input',\n  'link',\n  'meta',\n  'source',\n  'track',\n  'wbr',\n]);\n\nexport type HTMLValidationIssue = {\n  type: 'error' | 'warning';\n  message: string;\n};\n\nexport type HTMLValidationResult = {\n  valid: boolean;\n  issues: HTMLValidationIssue[];\n};\n\n// Matches HTML tags: <Tag ...>, </Tag>, or <Tag ... />\n// Attributes may span multiple lines but NOT blank lines (two consecutive newlines),\n// which prevents matching blockquote `>` markers as tag closers.\n// Groups: 1: closing slash, 2: tag name, 3: attributes, 4: self-closing slash\nconst TAG_REGEX =\n  /<(\\/)?([a-zA-Z][a-zA-Z0-9.-]*)\\s*((?:[^\\n]|\\n(?!\\n))*?)(\\/?)>/g;\n\n/**\n * Validates that HTML tags in `content` are properly opened, nested, and closed.\n * Returns structured issues instead of logging to the console.\n *\n * False-positive exclusions:\n * - Tags whose attribute string starts with `://` are URL autolinks (e.g. `<https://…>`).\n */\nexport const validateHTML = (content: string): HTMLValidationResult => {\n  const issues: HTMLValidationIssue[] = [];\n  const stack: Array<{ tag: string }> = [];\n\n  for (const match of content.matchAll(TAG_REGEX)) {\n    const isClosing = !!match[1];\n    const tagName = match[2];\n    const attrs = match[3];\n    const isSelfClosing = !!match[4];\n\n    // Skip URL autolinks like <https://example.com> or <mailto:user@example.com>\n    if (\n      attrs.trimStart().startsWith('://') ||\n      attrs.trimStart().startsWith(':')\n    ) {\n      continue;\n    }\n\n    if (isClosing) {\n      if (stack.length === 0) {\n        issues.push({\n          type: 'error',\n          message: `Closing tag </${tagName}> has no matching opening tag`,\n        });\n      } else {\n        const last = stack[stack.length - 1];\n        if (last.tag.toLowerCase() !== tagName.toLowerCase()) {\n          issues.push({\n            type: 'error',\n            message: `Mismatched closing tag: expected </${last.tag}> but found </${tagName}>`,\n          });\n        }\n        stack.pop();\n      }\n    } else {\n      const isVoidElement = VOID_HTML_ELEMENTS.has(tagName.toLowerCase());\n      if (!isSelfClosing && !isVoidElement) {\n        stack.push({ tag: tagName });\n      }\n    }\n  }\n\n  for (const unclosed of stack) {\n    issues.push({\n      type: 'error',\n      message: `Unclosed HTML tag: <${unclosed.tag}>`,\n    });\n  }\n\n  return {\n    valid: issues.filter((i) => i.type === 'error').length === 0,\n    issues,\n  };\n};\n"],"mappings":";;;AAAA,MAAa,qBAAqB,IAAI,IAAI;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAgBF,MAAM,YACJ;;;;;;;;AASF,MAAa,gBAAgB,YAA0C;CACrE,MAAM,SAAgC,EAAE;CACxC,MAAM,QAAgC,EAAE;AAExC,MAAK,MAAM,SAAS,QAAQ,SAAS,UAAU,EAAE;EAC/C,MAAM,YAAY,CAAC,CAAC,MAAM;EAC1B,MAAM,UAAU,MAAM;EACtB,MAAM,QAAQ,MAAM;EACpB,MAAM,gBAAgB,CAAC,CAAC,MAAM;AAG9B,MACE,MAAM,WAAW,CAAC,WAAW,MAAM,IACnC,MAAM,WAAW,CAAC,WAAW,IAAI,CAEjC;AAGF,MAAI,UACF,KAAI,MAAM,WAAW,EACnB,QAAO,KAAK;GACV,MAAM;GACN,SAAS,iBAAiB,QAAQ;GACnC,CAAC;OACG;GACL,MAAM,OAAO,MAAM,MAAM,SAAS;AAClC,OAAI,KAAK,IAAI,aAAa,KAAK,QAAQ,aAAa,CAClD,QAAO,KAAK;IACV,MAAM;IACN,SAAS,sCAAsC,KAAK,IAAI,gBAAgB,QAAQ;IACjF,CAAC;AAEJ,SAAM,KAAK;;OAER;GACL,MAAM,gBAAgB,mBAAmB,IAAI,QAAQ,aAAa,CAAC;AACnE,OAAI,CAAC,iBAAiB,CAAC,cACrB,OAAM,KAAK,EAAE,KAAK,SAAS,CAAC;;;AAKlC,MAAK,MAAM,YAAY,MACrB,QAAO,KAAK;EACV,MAAM;EACN,SAAS,uBAAuB,SAAS,IAAI;EAC9C,CAAC;AAGJ,QAAO;EACL,OAAO,OAAO,QAAQ,MAAM,EAAE,SAAS,QAAQ,CAAC,WAAW;EAC3D;EACD"}