UNPKG

@intlayer/core

Version:

Includes core Intlayer functions like translation, dictionary, and utility functions shared across multiple packages.

1 lines 3.69 kB
{"version":3,"file":"validateHTML.mjs","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"}