import { Visitor, isERBNode } from '@herb-tools/core';

/**
 * Base visitor class that provides common functionality for rule visitors
 */
class BaseRuleVisitor extends Visitor {
    offenses = [];
    ruleName;
    constructor(ruleName) {
        super();
        this.ruleName = ruleName;
    }
    /**
     * Helper method to create a lint offense
     */
    createOffense(message, location, severity = "error") {
        return {
            rule: this.ruleName,
            code: this.ruleName,
            source: "Herb Linter",
            message,
            location,
            severity,
        };
    }
    /**
     * Helper method to add an offense to the offenses array
     */
    addOffense(message, location, severity = "error") {
        this.offenses.push(this.createOffense(message, location, severity));
    }
}
/**
 * Gets attributes from either an HTMLOpenTagNode or HTMLSelfCloseTagNode
 */
function getAttributes(node) {
    return node.type === "AST_HTML_SELF_CLOSE_TAG_NODE"
        ? node.attributes
        : node.children;
}
/**
 * Gets the tag name from an HTML tag node (lowercased)
 */
function getTagName(node) {
    return node.tag_name?.value.toLowerCase() || null;
}
/**
 * Gets the attribute name from an HTMLAttributeNode (lowercased)
 */
function getAttributeName(attributeNode) {
    if (attributeNode.name?.type === "AST_HTML_ATTRIBUTE_NAME_NODE") {
        const nameNode = attributeNode.name;
        return nameNode.name?.value.toLowerCase() || null;
    }
    return null;
}
/**
 * Gets the attribute value content from an HTMLAttributeValueNode
 */
function getAttributeValue(attributeNode) {
    if (attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
        const valueNode = attributeNode.value;
        if (valueNode.children && valueNode.children.length > 0) {
            return valueNode.children
                .filter(child => child.type === "AST_LITERAL_NODE")
                .map(child => child.content)
                .join("");
        }
    }
    return null;
}
/**
 * Checks if an attribute has a value
 */
function hasAttributeValue(attributeNode) {
    return attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE";
}
/**
 * Gets the quote type used for an attribute value
 */
function getAttributeValueQuoteType(attributeNode) {
    if (attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
        const valueNode = attributeNode.value;
        if (valueNode.quoted && valueNode.open_quote) {
            return valueNode.open_quote.value === '"' ? "double" : "single";
        }
        return "none";
    }
    return null;
}
/**
 * Finds an attribute by name in a list of attributes
 */
function findAttributeByName(attributes, attributeName) {
    for (const child of attributes) {
        if (child.type === "AST_HTML_ATTRIBUTE_NODE") {
            const attributeNode = child;
            const name = getAttributeName(attributeNode);
            if (name === attributeName.toLowerCase()) {
                return attributeNode;
            }
        }
    }
    return null;
}
/**
 * Checks if a tag has a specific attribute
 */
function hasAttribute(node, attributeName) {
    const attributes = getAttributes(node);
    return findAttributeByName(attributes, attributeName) !== null;
}
/**
 * Common HTML element categorization
 */
const HTML_INLINE_ELEMENTS = new Set([
    "a", "abbr", "acronym", "b", "bdo", "big", "br", "button", "cite", "code",
    "dfn", "em", "i", "img", "input", "kbd", "label", "map", "object", "output",
    "q", "samp", "script", "select", "small", "span", "strong", "sub", "sup",
    "textarea", "time", "tt", "var"
]);
const HTML_BLOCK_ELEMENTS = new Set([
    "address", "article", "aside", "blockquote", "canvas", "dd", "div", "dl",
    "dt", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2",
    "h3", "h4", "h5", "h6", "header", "hr", "li", "main", "nav", "noscript",
    "ol", "p", "pre", "section", "table", "tfoot", "ul", "video"
]);
const HTML_BOOLEAN_ATTRIBUTES = new Set([
    "autofocus", "autoplay", "checked", "controls", "defer", "disabled", "hidden",
    "loop", "multiple", "muted", "readonly", "required", "reversed", "selected",
    "open", "default", "formnovalidate", "novalidate", "itemscope", "scoped",
    "seamless", "allowfullscreen", "async", "compact", "declare", "nohref",
    "noresize", "noshade", "nowrap", "sortable", "truespeed", "typemustmatch"
]);
const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
/**
 * Checks if an element is inline
 */
function isInlineElement(tagName) {
    return HTML_INLINE_ELEMENTS.has(tagName.toLowerCase());
}
/**
 * Checks if an element is block-level
 */
function isBlockElement(tagName) {
    return HTML_BLOCK_ELEMENTS.has(tagName.toLowerCase());
}
/**
 * Checks if an attribute is a boolean attribute
 */
function isBooleanAttribute(attributeName) {
    return HTML_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase());
}
/**
 * Abstract base class for rules that need to check individual attributes on HTML tags
 * Eliminates duplication of visitHTMLOpenTagNode/visitHTMLSelfCloseTagNode patterns
 * and attribute iteration logic. Provides simplified interface with extracted attribute info.
 */
class AttributeVisitorMixin extends BaseRuleVisitor {
    visitHTMLOpenTagNode(node) {
        this.checkAttributesOnNode(node);
        super.visitHTMLOpenTagNode(node);
    }
    visitHTMLSelfCloseTagNode(node) {
        this.checkAttributesOnNode(node);
        super.visitHTMLSelfCloseTagNode(node);
    }
    checkAttributesOnNode(node) {
        forEachAttribute(node, (attributeNode) => {
            const attributeName = getAttributeName(attributeNode);
            const attributeValue = getAttributeValue(attributeNode);
            if (attributeName) {
                this.checkAttribute(attributeName, attributeValue, attributeNode, node);
            }
        });
    }
}
/**
 * Iterates over all attributes of a tag node, calling the callback for each attribute
 */
function forEachAttribute(node, callback) {
    const attributes = getAttributes(node);
    for (const child of attributes) {
        if (child.type === "AST_HTML_ATTRIBUTE_NODE") {
            callback(child);
        }
    }
}

class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
    visitERBContentNode(node) {
        this.visitChildNodes(node);
        const { content, tag_closing } = node;
        if (!content)
            return;
        if (tag_closing?.value === "")
            return;
        if (content.value.trim().length > 0)
            return;
        this.addOffense("ERB tag should not be empty. Remove empty ERB tags or add content.", node.location, "error");
    }
}
class ERBNoEmptyTagsRule {
    name = "erb-no-empty-tags";
    check(node) {
        const visitor = new ERBNoEmptyTagsVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class ERBNoOutputControlFlowRuleVisitor extends BaseRuleVisitor {
    visitERBIfNode(node) {
        this.checkOutputControlFlow(node);
        this.visitChildNodes(node);
    }
    visitERBUnlessNode(node) {
        this.checkOutputControlFlow(node);
        this.visitChildNodes(node);
    }
    visitERBElseNode(node) {
        this.checkOutputControlFlow(node);
        this.visitChildNodes(node);
    }
    visitERBEndNode(node) {
        this.checkOutputControlFlow(node);
        this.visitChildNodes(node);
    }
    checkOutputControlFlow(controlBlock) {
        const openTag = controlBlock.tag_opening;
        if (!openTag) {
            return;
        }
        if (openTag.value === "<%=") {
            let controlBlockType = controlBlock.type;
            if (controlBlock.type === "AST_ERB_IF_NODE")
                controlBlockType = "if";
            if (controlBlock.type === "AST_ERB_ELSE_NODE")
                controlBlockType = "else";
            if (controlBlock.type === "AST_ERB_END_NODE")
                controlBlockType = "end";
            if (controlBlock.type === "AST_ERB_UNLESS_NODE")
                controlBlockType = "unless";
            this.addOffense(`Control flow statements like \`${controlBlockType}\` should not be used with output tags. Use \`<% ${controlBlockType} ... %>\` instead.`, openTag.location, "error");
        }
        return;
    }
}
class ERBNoOutputControlFlowRule {
    name = "erb-no-output-control-flow";
    check(node) {
        const visitor = new ERBNoOutputControlFlowRuleVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class RequireWhitespaceInsideTags extends BaseRuleVisitor {
    visitChildNodes(node) {
        this.checkWhitespace(node);
        super.visitChildNodes(node);
    }
    checkWhitespace(node) {
        if (!isERBNode(node)) {
            return;
        }
        const openTag = node.tag_opening;
        const closeTag = node.tag_closing;
        const content = node.content;
        if (!openTag || !closeTag || !content) {
            return;
        }
        const value = content.value;
        this.checkOpenTagWhitespace(openTag, value);
        this.checkCloseTagWhitespace(closeTag, value);
    }
    checkOpenTagWhitespace(openTag, content) {
        if (content.startsWith(" ") || content.startsWith("\n")) {
            return;
        }
        this.addOffense(`Add whitespace after \`${openTag.value}\`.`, openTag.location, "error");
    }
    checkCloseTagWhitespace(closeTag, content) {
        if (content.endsWith(" ") || content.endsWith("\n")) {
            return;
        }
        this.addOffense(`Add whitespace before \`${closeTag.value}\`.`, closeTag.location, "error");
    }
}
class ERBRequireWhitespaceRule {
    name = "erb-require-whitespace-inside-tags";
    check(node) {
        const visitor = new RequireWhitespaceInsideTags(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class AnchorRechireHrefVisitor extends BaseRuleVisitor {
    visitHTMLOpenTagNode(node) {
        this.checkATag(node);
        super.visitHTMLOpenTagNode(node);
    }
    checkATag(node) {
        const tagName = getTagName(node);
        if (tagName !== "a") {
            return;
        }
        if (!hasAttribute(node, "href")) {
            this.addOffense("Add an `href` attribute to `<a>` to ensure it is focusable and accessible.", node.tag_name.location, "error");
        }
    }
}
class HTMLAnchorRequireHrefRule {
    name = "html-anchor-require-href";
    check(node) {
        const visitor = new AnchorRechireHrefVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class AriaRoleHeadingRequiresLevel extends AttributeVisitorMixin {
    // We want to check 2 attributes here:
    // 1. role="heading"
    // 2. aria-level (which must be present if role="heading")
    checkAttribute(attributeName, attributeValue, attributeNode, parentNode) {
        if (!(attributeName === "role" && attributeValue === "heading")) {
            return;
        }
        const allAttributes = getAttributes(parentNode);
        // If we have a role="heading", we must check for aria-level
        const ariaLevelAttr = allAttributes.find(attr => getAttributeName(attr) === "aria-level");
        if (!ariaLevelAttr) {
            this.addOffense(`Element with \`role="heading"\` must have an \`aria-level\` attribute.`, attributeNode.location, "error");
        }
    }
}
class HTMLAriaRoleHeadingRequiresLevelRule {
    name = "html-aria-role-heading-requires-level";
    check(node) {
        const visitor = new AriaRoleHeadingRequiresLevel(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class AttributeDoubleQuotesVisitor extends AttributeVisitorMixin {
    checkAttribute(attributeName, attributeValue, attributeNode) {
        if (!hasAttributeValue(attributeNode))
            return;
        if (getAttributeValueQuoteType(attributeNode) !== "single")
            return;
        if (attributeValue?.includes('"'))
            return; // Single quotes acceptable when value contains double quotes
        this.addOffense(`Attribute \`${attributeName}\` uses single quotes. Prefer double quotes for HTML attribute values: \`${attributeName}="value"\`.`, attributeNode.value.location, "warning");
    }
}
class HTMLAttributeDoubleQuotesRule {
    name = "html-attribute-double-quotes";
    check(node) {
        const visitor = new AttributeDoubleQuotesVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class AttributeValuesRequireQuotesVisitor extends AttributeVisitorMixin {
    checkAttribute(attributeName, _attributeValue, attributeNode) {
        if (attributeNode.value?.type !== "AST_HTML_ATTRIBUTE_VALUE_NODE")
            return;
        const valueNode = attributeNode.value;
        if (valueNode.quoted)
            return;
        this.addOffense(
        // TODO: print actual attribute value in message
        `Attribute value should be quoted: \`${attributeName}="value"\`. Always wrap attribute values in quotes.`, valueNode.location, "error");
    }
}
class HTMLAttributeValuesRequireQuotesRule {
    name = "html-attribute-values-require-quotes";
    check(node) {
        const visitor = new AttributeValuesRequireQuotesVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
    checkAttribute(attributeName, _attributeValue, attributeNode) {
        if (!isBooleanAttribute(attributeName))
            return;
        if (!hasAttributeValue(attributeNode))
            return;
        this.addOffense(`Boolean attribute \`${attributeName}\` should not have a value. Use \`${attributeName}\` instead of \`${attributeName}="${attributeName}"\`.`, attributeNode.value.location, "error");
    }
}
class HTMLBooleanAttributesNoValueRule {
    name = "html-boolean-attributes-no-value";
    check(node) {
        const visitor = new BooleanAttributesNoValueVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class ImgRequireAltVisitor extends BaseRuleVisitor {
    visitHTMLOpenTagNode(node) {
        this.checkImgTag(node);
        super.visitHTMLOpenTagNode(node);
    }
    visitHTMLSelfCloseTagNode(node) {
        this.checkImgTag(node);
        super.visitHTMLSelfCloseTagNode(node);
    }
    checkImgTag(node) {
        const tagName = getTagName(node);
        if (tagName !== "img") {
            return;
        }
        if (!hasAttribute(node, "alt")) {
            this.addOffense('Missing required `alt` attribute on `<img>` tag. Add `alt=""` for decorative images or `alt="description"` for informative images.', node.tag_name.location, "error");
        }
    }
}
class HTMLImgRequireAltRule {
    name = "html-img-require-alt";
    check(node) {
        const visitor = new ImgRequireAltVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class BlockInsideInlineVisitor extends BaseRuleVisitor {
    inlineStack = [];
    isValidHTMLOpenTag(node) {
        return !!(node.open_tag && node.open_tag.type === "AST_HTML_OPEN_TAG_NODE");
    }
    getElementType(tagName) {
        const isInline = isInlineElement(tagName);
        const isBlock = isBlockElement(tagName);
        const isUnknown = !isInline && !isBlock;
        return { isInline, isBlock, isUnknown };
    }
    addViolationMessage(tagName, isBlock, openTag) {
        const parentInline = this.inlineStack[this.inlineStack.length - 1];
        const elementType = isBlock ? "Block-level" : "Unknown";
        this.addOffense(`${elementType} element \`<${tagName}>\` cannot be placed inside inline element \`<${parentInline}>\`.`, openTag.tag_name.location, "error");
    }
    visitInlineElement(node, tagName) {
        this.inlineStack.push(tagName);
        super.visitHTMLElementNode(node);
        this.inlineStack.pop();
    }
    visitBlockElement(node) {
        const savedStack = [...this.inlineStack];
        this.inlineStack = [];
        super.visitHTMLElementNode(node);
        this.inlineStack = savedStack;
    }
    visitHTMLElementNode(node) {
        if (!this.isValidHTMLOpenTag(node)) {
            super.visitHTMLElementNode(node);
            return;
        }
        const openTag = node.open_tag;
        const tagName = openTag.tag_name?.value.toLowerCase();
        if (!tagName) {
            super.visitHTMLElementNode(node);
            return;
        }
        const { isInline, isBlock, isUnknown } = this.getElementType(tagName);
        if ((isBlock || isUnknown) && this.inlineStack.length > 0) {
            this.addViolationMessage(tagName, isBlock, openTag);
        }
        if (isInline) {
            this.visitInlineElement(node, tagName);
            return;
        }
        this.visitBlockElement(node);
    }
}
class HTMLNoBlockInsideInlineRule {
    name = "html-no-block-inside-inline";
    check(node) {
        const visitor = new BlockInsideInlineVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class NoDuplicateAttributesVisitor extends BaseRuleVisitor {
    visitHTMLOpenTagNode(node) {
        this.checkDuplicateAttributes(node);
        super.visitHTMLOpenTagNode(node);
    }
    visitHTMLSelfCloseTagNode(node) {
        this.checkDuplicateAttributes(node);
        super.visitHTMLSelfCloseTagNode(node);
    }
    checkDuplicateAttributes(node) {
        const attributeNames = new Map();
        forEachAttribute(node, (attributeNode) => {
            if (attributeNode.name?.type !== "AST_HTML_ATTRIBUTE_NAME_NODE")
                return;
            const nameNode = attributeNode.name;
            if (!nameNode.name)
                return;
            const attributeName = nameNode.name.value.toLowerCase(); // HTML attributes are case-insensitive
            if (!attributeNames.has(attributeName)) {
                attributeNames.set(attributeName, []);
            }
            attributeNames.get(attributeName).push(nameNode);
        });
        for (const [attributeName, nameNodes] of attributeNames) {
            if (nameNodes.length > 1) {
                for (let i = 1; i < nameNodes.length; i++) {
                    const nameNode = nameNodes[i];
                    this.addOffense(`Duplicate attribute \`${attributeName}\` found on tag. Remove the duplicate occurrence.`, nameNode.location, "error");
                }
            }
        }
    }
}
class HTMLNoDuplicateAttributesRule {
    name = "html-no-duplicate-attributes";
    check(node) {
        const visitor = new NoDuplicateAttributesVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
    visitHTMLElementNode(node) {
        this.checkHeadingElement(node);
        super.visitHTMLElementNode(node);
    }
    visitHTMLSelfCloseTagNode(node) {
        this.checkSelfClosingHeading(node);
        super.visitHTMLSelfCloseTagNode(node);
    }
    checkHeadingElement(node) {
        if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
            return;
        }
        const openTag = node.open_tag;
        const tagName = getTagName(openTag);
        if (!tagName) {
            return;
        }
        const isStandardHeading = HEADING_TAGS.has(tagName);
        const isAriaHeading = this.hasHeadingRole(openTag);
        if (!isStandardHeading && !isAriaHeading) {
            return;
        }
        if (this.isEmptyHeading(node)) {
            const elementDescription = isStandardHeading
                ? `\`<${tagName}>\``
                : `\`<${tagName} role="heading">\``;
            this.addOffense(`Heading element ${elementDescription} must not be empty. Provide accessible text content for screen readers and SEO.`, node.location, "error");
        }
    }
    checkSelfClosingHeading(node) {
        const tagName = getTagName(node);
        if (!tagName) {
            return;
        }
        // Check if it's a standard heading tag (h1-h6) or has role="heading"
        const isStandardHeading = HEADING_TAGS.has(tagName);
        const isAriaHeading = this.hasHeadingRole(node);
        if (!isStandardHeading && !isAriaHeading) {
            return;
        }
        // Self-closing headings are always empty
        const elementDescription = isStandardHeading
            ? `\`<${tagName}>\``
            : `\`<${tagName} role="heading">\``;
        this.addOffense(`Heading element ${elementDescription} must not be empty. Provide accessible text content for screen readers and SEO.`, node.tag_name.location, "error");
    }
    isEmptyHeading(node) {
        if (!node.body || node.body.length === 0) {
            return true;
        }
        // Check if all content is just whitespace or inaccessible
        let hasAccessibleContent = false;
        for (const child of node.body) {
            if (child.type === "AST_LITERAL_NODE") {
                const literalNode = child;
                if (literalNode.content.trim().length > 0) {
                    hasAccessibleContent = true;
                    break;
                }
            }
            else if (child.type === "AST_HTML_TEXT_NODE") {
                const textNode = child;
                if (textNode.content.trim().length > 0) {
                    hasAccessibleContent = true;
                    break;
                }
            }
            else if (child.type === "AST_HTML_ELEMENT_NODE") {
                const elementNode = child;
                // Check if this element is accessible (not aria-hidden="true")
                if (this.isElementAccessible(elementNode)) {
                    hasAccessibleContent = true;
                    break;
                }
            }
            else {
                // If there's any non-literal/non-text/non-element content (like ERB), consider it accessible
                hasAccessibleContent = true;
                break;
            }
        }
        return !hasAccessibleContent;
    }
    hasHeadingRole(node) {
        const attributes = getAttributes(node);
        const roleAttribute = findAttributeByName(attributes, "role");
        if (!roleAttribute) {
            return false;
        }
        const roleValue = getAttributeValue(roleAttribute);
        return roleValue === "heading";
    }
    isElementAccessible(node) {
        // Check if the element has aria-hidden="true"
        if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
            return true;
        }
        const openTag = node.open_tag;
        const attributes = getAttributes(openTag);
        const ariaHiddenAttribute = findAttributeByName(attributes, "aria-hidden");
        if (ariaHiddenAttribute) {
            const ariaHiddenValue = getAttributeValue(ariaHiddenAttribute);
            if (ariaHiddenValue === "true") {
                return false;
            }
        }
        // Recursively check if the element has any accessible content
        if (!node.body || node.body.length === 0) {
            return false;
        }
        for (const child of node.body) {
            if (child.type === "AST_LITERAL_NODE") {
                const literalNode = child;
                if (literalNode.content.trim().length > 0) {
                    return true;
                }
            }
            else if (child.type === "AST_HTML_TEXT_NODE") {
                const textNode = child;
                if (textNode.content.trim().length > 0) {
                    return true;
                }
            }
            else if (child.type === "AST_HTML_ELEMENT_NODE") {
                const elementNode = child;
                if (this.isElementAccessible(elementNode)) {
                    return true;
                }
            }
            else {
                // If there's any non-literal/non-text/non-element content (like ERB), consider it accessible
                return true;
            }
        }
        return false;
    }
}
class HTMLNoEmptyHeadingsRule {
    name = "html-no-empty-headings";
    check(node) {
        const visitor = new NoEmptyHeadingsVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class NestedLinkVisitor extends BaseRuleVisitor {
    linkStack = [];
    checkNestedLink(openTag) {
        if (this.linkStack.length > 0) {
            this.addOffense("Nested `<a>` elements are not allowed. Links cannot contain other links.", openTag.tag_name.location, "error");
            return true;
        }
        return false;
    }
    visitHTMLElementNode(node) {
        if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
            super.visitHTMLElementNode(node);
            return;
        }
        const openTag = node.open_tag;
        const tagName = getTagName(openTag);
        if (tagName !== "a") {
            super.visitHTMLElementNode(node);
            return;
        }
        // If we're already inside a link, this is a nested link
        this.checkNestedLink(openTag);
        this.linkStack.push(openTag);
        super.visitHTMLElementNode(node);
        this.linkStack.pop();
    }
    // Handle self-closing <a> tags (though they're not valid HTML, they might exist)
    visitHTMLOpenTagNode(node) {
        const tagName = getTagName(node);
        if (tagName === "a" && node.is_void) {
            this.checkNestedLink(node);
        }
        super.visitHTMLOpenTagNode(node);
    }
}
class HTMLNoNestedLinksRule {
    name = "html-no-nested-links";
    check(node) {
        const visitor = new NestedLinkVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

class TagNameLowercaseVisitor extends BaseRuleVisitor {
    visitHTMLOpenTagNode(node) {
        this.checkTagName(node);
        this.visitChildNodes(node);
    }
    visitHTMLCloseTagNode(node) {
        this.checkTagName(node);
        this.visitChildNodes(node);
    }
    visitHTMLSelfCloseTagNode(node) {
        this.checkTagName(node);
        this.visitChildNodes(node);
    }
    checkTagName(node) {
        const tagName = node.tag_name?.value;
        if (!tagName)
            return;
        if (tagName !== tagName.toLowerCase()) {
            let type = node.type;
            if (node.type == "AST_HTML_OPEN_TAG_NODE")
                type = "Opening";
            if (node.type == "AST_HTML_CLOSE_TAG_NODE")
                type = "Closing";
            if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE")
                type = "Self-closing";
            this.addOffense(`${type} tag name \`${tagName}\` should be lowercase. Use \`${tagName.toLowerCase()}\` instead.`, node.tag_name.location, "error");
        }
    }
}
class HTMLTagNameLowercaseRule {
    name = "html-tag-name-lowercase";
    check(node) {
        const visitor = new TagNameLowercaseVisitor(this.name);
        visitor.visit(node);
        return visitor.offenses;
    }
}

const defaultRules = [
    ERBNoEmptyTagsRule,
    ERBNoOutputControlFlowRule,
    ERBRequireWhitespaceRule,
    HTMLAnchorRequireHrefRule,
    HTMLAriaRoleHeadingRequiresLevelRule,
    HTMLAttributeDoubleQuotesRule,
    HTMLAttributeValuesRequireQuotesRule,
    HTMLBooleanAttributesNoValueRule,
    HTMLImgRequireAltRule,
    HTMLNoBlockInsideInlineRule,
    HTMLNoDuplicateAttributesRule,
    HTMLNoEmptyHeadingsRule,
    HTMLNoNestedLinksRule,
    HTMLTagNameLowercaseRule,
];

class Linter {
    rules;
    offenses;
    /**
     * Creates a new Linter instance.
     * @param rules - Array of rule classes (not instances) to use. If not provided, uses default rules.
     */
    constructor(rules) {
        this.rules = rules !== undefined ? rules : this.getDefaultRules();
        this.offenses = [];
    }
    /**
     * Returns the default set of rule classes used by the linter.
     * @returns Array of rule classes
     */
    getDefaultRules() {
        return defaultRules;
    }
    getRuleCount() {
        return this.rules.length;
    }
    lint(document) {
        this.offenses = [];
        for (const Rule of this.rules) {
            const rule = new Rule();
            const ruleOffenses = rule.check(document);
            this.offenses.push(...ruleOffenses);
        }
        const errors = this.offenses.filter(offense => offense.severity === "error").length;
        const warnings = this.offenses.filter(offense => offense.severity === "warning").length;
        return {
            offenses: this.offenses,
            errors,
            warnings
        };
    }
}

export { ERBNoEmptyTagsRule, ERBNoOutputControlFlowRule, HTMLAnchorRequireHrefRule, HTMLAttributeDoubleQuotesRule, HTMLAttributeValuesRequireQuotesRule, HTMLBooleanAttributesNoValueRule, HTMLImgRequireAltRule, HTMLNoBlockInsideInlineRule, HTMLNoDuplicateAttributesRule, HTMLNoEmptyHeadingsRule, HTMLNoNestedLinksRule, HTMLTagNameLowercaseRule, Linter };
//# sourceMappingURL=index.js.map
