tree-sitter-htmlx 0.1.13

Tree-sitter grammar for HTMLX (expression-enhanced HTML)
Documentation
//! Tests for HTMLX elements

mod utils;
use utils::parse;

// =============================================================================
// Basic elements
// =============================================================================

#[test]
fn test_element_simple() {
    assert_eq!(
        parse("<div></div>"),
        "(document (element (start_tag name: (tag_name)) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_element_self_closing() {
    assert_eq!(
        parse("<br />"),
        "(document (element (self_closing_tag name: (tag_name))))"
    );
}

#[test]
fn test_element_with_text() {
    assert_eq!(
        parse("<p>Hello</p>"),
        "(document (element (start_tag name: (tag_name)) (text) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_element_nested() {
    assert_eq!(
        parse("<div><span>text</span></div>"),
        "(document (element (start_tag name: (tag_name)) (element (start_tag name: (tag_name)) (text) (end_tag name: (tag_name))) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_erroneous_end_tag_is_typed() {
    assert_eq!(
        parse("</div>"),
        "(document (erroneous_end_tag (erroneous_end_tag_name)))"
    );
}

#[test]
fn test_title_element_allows_expression_children() {
    assert_eq!(
        parse("<title>{pageTitle}</title>"),
        "(document (element (start_tag name: (tag_name)) (expression content: (js)) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_title_element_preserves_nested_elements_for_validation() {
    assert_eq!(
        parse("<title><span>bad</span></title>"),
        "(document (element (start_tag name: (tag_name)) (element (start_tag name: (tag_name)) (text) (end_tag name: (tag_name))) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_textarea_plain_text_stays_text_like() {
    assert_eq!(
        parse("<textarea>plain <b>text</b></textarea>"),
        "(document (element (start_tag name: (tag_name)) (text) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_textarea_expression_exposes_htmlx_expression() {
    assert_eq!(
        parse("<textarea>{value}</textarea>"),
        "(document (element (start_tag name: (tag_name)) (expression content: (js)) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_textarea_ignores_malformed_close_until_real_end_tag() {
    assert_eq!(
        parse("<textarea>x</textarea\n\n\n</textarea\n\n>"),
        "(document (element (start_tag name: (tag_name)) (text) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_void_element_closes_before_following_expression() {
    assert_eq!(
        parse("<label><input>{value}</label>"),
        "(document (element (start_tag name: (tag_name)) (element (start_tag name: (tag_name))) (expression content: (js)) (end_tag name: (tag_name))))"
    );
}

// =============================================================================
// Component elements (PascalCase)
// =============================================================================

#[test]
fn test_component_element() {
    assert_eq!(
        parse("<MyComponent />"),
        "(document (element (self_closing_tag name: (tag_name))))"
    );
}

#[test]
fn test_component_element_with_unicode_name() {
    assert_eq!(
        parse("<Wunderschön />"),
        "(document (element (self_closing_tag name: (tag_name))))"
    );
}

#[test]
fn test_component_with_children() {
    assert_eq!(
        parse("<Layout><Content /></Layout>"),
        "(document (element (start_tag name: (tag_name)) (element (self_closing_tag name: (tag_name))) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_component_dotted() {
    // Dotted components are parsed with object and property fields containing tag_member nodes
    assert_eq!(
        parse("<UI.Button />"),
        "(document (element (self_closing_tag name: (tag_name object: (tag_member) property: (tag_member)))))"
    );
}

#[test]
fn test_component_dotted_with_unicode_property() {
    assert_eq!(
        parse("<UI.Schön />"),
        "(document (element (self_closing_tag name: (tag_name object: (tag_member) property: (tag_member)))))"
    );
}

#[test]
fn test_component_dotted_with_children() {
    assert_eq!(
        parse("<UI.Card>content</UI.Card>"),
        "(document (element (start_tag name: (tag_name object: (tag_member) property: (tag_member))) (text) (end_tag name: (tag_name object: (tag_member) property: (tag_member)))))"
    );
}

#[test]
fn test_component_deeply_dotted() {
    // Multiple levels of dotting: object + multiple property fields
    assert_eq!(
        parse("<Lib.UI.Button />"),
        "(document (element (self_closing_tag name: (tag_name object: (tag_member) property: (tag_member) property: (tag_member)))))"
    );
}

// =============================================================================
// Namespaced elements
// =============================================================================

#[test]
fn test_namespaced_element() {
    assert_eq!(
        parse("<svelte:head></svelte:head>"),
        "(document (element (start_tag name: (tag_name namespace: (tag_namespace) name: (tag_local_name))) (end_tag name: (tag_name namespace: (tag_namespace) name: (tag_local_name)))))"
    );
}

#[test]
fn test_custom_element_with_unicode_suffix() {
    assert_eq!(
        parse("<math-α></math-α>"),
        "(document (element (start_tag name: (tag_name)) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_invalid_element_name_with_brackets_stays_single_tag_name() {
    assert_eq!(
        parse("<yes[no]></yes[no]>"),
        "(document (element (start_tag name: (tag_name)) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_namespaced_self_closing() {
    assert_eq!(
        parse("<svelte:self />"),
        "(document (element (self_closing_tag name: (tag_name namespace: (tag_namespace) name: (tag_local_name)))))"
    );
}

#[test]
fn test_namespaced_self_closing_inside_element() {
    // Namespaced self-closing tags must not pop the parent from the tag stack
    assert_eq!(
        parse("<div><svelte:window/></div>"),
        "(document (element (start_tag name: (tag_name)) (element (self_closing_tag name: (tag_name namespace: (tag_namespace) name: (tag_local_name)))) (end_tag name: (tag_name))))"
    );
}

// =============================================================================
// Unterminated start tags with matching close tags
// =============================================================================

#[test]
fn test_unterminated_tag_with_comment_then_close() {
    // <div //comment\n</div> should produce a proper element, not standalone + erroneous end tag
    assert_eq!(
        parse("<div //comment\n</div>"),
        "(document (element (start_tag name: (tag_name) (tag_comment kind: (line_comment))) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_unterminated_tag_with_attr_and_comment_then_close() {
    assert_eq!(
        parse("<div class=\"x\" //comment\n</div>"),
        "(document (element (start_tag name: (tag_name) (attribute name: (attribute_name) value: (quoted_attribute_value (attribute_value))) (tag_comment kind: (line_comment))) (end_tag name: (tag_name))))"
    );
}

// =============================================================================
// Script/style/textarea raw text elements
// =============================================================================

#[test]
fn test_script_element() {
    assert_eq!(
        parse("<script>let x = 1;</script>"),
        "(document (element (start_tag name: (tag_name)) (raw_text) (end_tag name: (tag_name))))"
    );
}

#[test]
fn test_style_element() {
    assert_eq!(
        parse("<style>div { color: red; }</style>"),
        "(document (element (start_tag name: (tag_name)) (raw_text) (end_tag name: (tag_name))))"
    );
}