@intlayer/core
Version:
Includes core Intlayer functions like translation, dictionary, and utility functions shared across multiple packages.
63 lines (62 loc) • 1.76 kB
JavaScript
//#region src/transpiler/html/validateHTML.ts
const VOID_HTML_ELEMENTS = new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"source",
"track",
"wbr"
]);
const TAG_REGEX = /<(\/)?([a-zA-Z][a-zA-Z0-9.-]*)\s*((?:[^\n]|\n(?!\n))*?)(\/?)>/g;
/**
* Validates that HTML tags in `content` are properly opened, nested, and closed.
* Returns structured issues instead of logging to the console.
*
* False-positive exclusions:
* - Tags whose attribute string starts with `://` are URL autolinks (e.g. `<https://…>`).
*/
const validateHTML = (content) => {
const issues = [];
const stack = [];
for (const match of content.matchAll(TAG_REGEX)) {
const isClosing = !!match[1];
const tagName = match[2];
const attrs = match[3];
const isSelfClosing = !!match[4];
if (attrs.trimStart().startsWith("://") || attrs.trimStart().startsWith(":")) continue;
if (isClosing) if (stack.length === 0) issues.push({
type: "error",
message: `Closing tag </${tagName}> has no matching opening tag`
});
else {
const last = stack[stack.length - 1];
if (last.tag.toLowerCase() !== tagName.toLowerCase()) issues.push({
type: "error",
message: `Mismatched closing tag: expected </${last.tag}> but found </${tagName}>`
});
stack.pop();
}
else {
const isVoidElement = VOID_HTML_ELEMENTS.has(tagName.toLowerCase());
if (!isSelfClosing && !isVoidElement) stack.push({ tag: tagName });
}
}
for (const unclosed of stack) issues.push({
type: "error",
message: `Unclosed HTML tag: <${unclosed.tag}>`
});
return {
valid: issues.filter((i) => i.type === "error").length === 0,
issues
};
};
//#endregion
export { VOID_HTML_ELEMENTS, validateHTML };
//# sourceMappingURL=validateHTML.mjs.map