use super::*;
use crate::ast::types::AstNodeKind;
use crate::diagnostics::{CompilerErrorCode, SyntaxPluginContext, SyntaxPluginOptions};
fn make_ctx<'a>(input: &'a str, options: &'a SyntaxPluginOptions) -> SyntaxPluginContext<'a> {
SyntaxPluginContext {
input,
bytes: input.as_bytes(),
options,
diagnostics: Vec::new(),
}
}
fn feed<'a>(syntax: &mut Syntax, events: &[TokenizerEvent<'a>], ctx: &SyntaxPluginContext<'a>) {
for event in events {
syntax.handle(event, ctx);
}
}
fn tokenize_events(input: &str) -> Vec<TokenizerEvent<'static>> {
let mut events = Vec::new();
crate::tokenizer::byte::tokenize(input.as_bytes(), |event| {
events.push(event);
});
events
}
fn tokenize_and_feed(syntax: &mut Syntax, input: &str, ctx: &SyntaxPluginContext<'_>) {
let events = tokenize_events(input);
feed(syntax, &events, ctx);
}
fn tokenize_sfc_and_feed(syntax: &mut Syntax, input: &str, ctx: &SyntaxPluginContext<'_>) {
let mut events = Vec::new();
crate::tokenizer::byte::tokenize_sfc(input.as_bytes(), |event| {
events.push(event);
});
feed(syntax, &events, ctx);
}
fn span_str(input: &str, start: u32, end: u32) -> &str {
&input[start as usize..end as usize]
}
#[test]
fn script_basic() {
let input = "<script>console.log('hi')</script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let node = syn.script_node.as_ref().expect("script_node should exist");
assert_eq!(
span_str(input, node.tag_open.start + 1, node.tag_open.name_end),
"script"
);
assert!(!node.is_setup);
assert!(node.lang.is_none());
assert!(node.src.is_none());
let content = node.content.as_ref().expect("content should exist");
assert_eq!(
span_str(input, content.start, content.end),
"console.log('hi')"
);
assert!(syn.script_setup_node.is_none());
}
#[test]
fn script_setup_with_lang_ts() {
let input = "<script setup lang=\"ts\"></script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.script_node.is_none(),
"non-setup script_node should be None"
);
let node = syn
.script_setup_node
.as_ref()
.expect("script_setup_node should exist");
assert!(node.is_setup);
assert_eq!(node.lang, Some(ScriptLanguage::TypeScript));
}
#[test]
fn script_setup_flag_not_applied_to_style() {
let input = "<style setup></style>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert_eq!(syn.style_nodes.len(), 1);
assert!(!syn.style_nodes[0].scoped);
assert!(syn.script_setup_node.is_none());
}
#[test]
fn style_scoped_with_lang_scss() {
let input = "<style scoped lang=\"scss\"></style>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert_eq!(syn.style_nodes.len(), 1);
let style = &syn.style_nodes[0];
assert!(style.scoped);
assert!(!style.module);
assert_eq!(style.lang, Some(StyleLang::Scss));
assert!(syn.has_style_scope);
}
#[test]
fn style_module() {
let input = "<style module></style>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert_eq!(syn.style_nodes.len(), 1);
assert!(syn.style_nodes[0].module);
assert!(syn.has_style_module);
}
#[test]
fn scoped_flag_not_applied_to_script() {
let input = "<script scoped></script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let node = syn.script_node.as_ref().expect("script_node should exist");
assert!(!node.is_setup);
assert!(!syn.has_style_scope);
}
#[test]
fn template_basic_with_child() {
let input = "<template><div>hello</div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist");
assert!(ast.root.tag_close.is_some());
let content = ast.root.content.as_ref().expect("content should exist");
assert_eq!(
span_str(input, content.start, content.end),
"<div>hello</div>"
);
assert_eq!(content.children.len(), 1);
let div = &ast.nodes[content.children[0].0];
if let AstNodeKind::Element(el) = &div.kind {
let el_content = el.content.as_ref().unwrap();
assert_eq!(el_content.children.len(), 1);
let text = &ast.nodes[el_content.children[0].0];
assert!(matches!(text.kind, AstNodeKind::Text(_)));
} else {
panic!("expected Element, got {:?}", div.kind);
}
}
#[test]
fn element_tag_open_end_is_past_closing_bracket() {
let input = "<template><div>hello</div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist");
let content = ast.root.content.as_ref().expect("content should exist");
let div = &ast.nodes[content.children[0].0];
if let AstNodeKind::Element(el) = &div.kind {
assert_eq!(
el.tag_open.start, 10,
"tag_open.start should be '<' of <div>"
);
assert_eq!(
el.tag_open.end, 15,
"tag_open.end should be past '>' of <div>"
);
assert_eq!(span_str(input, el.tag_open.start, el.tag_open.end), "<div>");
} else {
panic!("expected Element, got {:?}", div.kind);
}
}
#[test]
fn self_closing_element_tag_open_end_is_past_closing_bracket() {
let input = "<template><br/></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist");
let content = ast.root.content.as_ref().expect("content should exist");
let br = &ast.nodes[content.children[0].0];
if let AstNodeKind::Element(el) = &br.kind {
assert_eq!(
el.tag_open.start, 10,
"tag_open.start should be '<' of <br/>"
);
assert_eq!(
el.tag_open.end, 15,
"tag_open.end should be past '/>' of <br/>"
);
assert_eq!(span_str(input, el.tag_open.start, el.tag_open.end), "<br/>");
assert!(el.is_self_closing);
} else {
panic!("expected Element, got {:?}", br.kind);
}
}
#[test]
fn template_self_closing() {
let input = "<template />";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist");
assert!(ast.root.tag_close.is_none());
assert!(ast.root.content.is_none());
}
#[test]
fn template_vapor_flag() {
let input = "<template vapor></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(syn.is_vapor);
assert!(syn.template_ast.is_some());
}
#[test]
fn unknown_root_node() {
let input = "<custom-block>data</custom-block>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert_eq!(syn.unknown_nodes.len(), 1);
let node = &syn.unknown_nodes[0];
let content = node.content.as_ref().expect("content should exist");
assert_eq!(span_str(input, content.start, content.end), "data");
}
#[test]
fn script_self_closing() {
let input = "<script src=\"./foo.ts\" />";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let node = syn.script_node.as_ref().expect("script_node should exist");
assert!(node.content.is_none());
assert!(node.tag_close.is_none());
let src = node.src.as_ref().expect("src should exist");
assert_eq!(span_str(input, src.start, src.end), "./foo.ts");
}
#[test]
fn template_mode_builds_ast_directly() {
let input = "<div><span>text</span></div>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(true);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(syn.script_node.is_none());
assert!(syn.style_nodes.is_empty());
assert!(
syn.ast_builder.is_none(),
"builder should be consumed by End event"
);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist after End event");
let root_content = ast.root.content.as_ref().unwrap();
assert_eq!(root_content.children.len(), 1);
}
#[test]
fn template_mode_no_root_prop_detection() {
let input = "<script setup></script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(true);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(syn.script_node.is_none());
assert!(syn.script_setup_node.is_none());
assert!(!syn.prop_setup);
}
#[test]
fn prop_state_preserved_until_close_for_script() {
let input = "<script setup lang=\"ts\">code</script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
let events = tokenize_events(input);
let split = events
.iter()
.position(|e| matches!(e, TokenizerEvent::OpenTagEnd { .. }))
.unwrap()
+ 1;
feed(&mut syn, &events[..split], &ctx);
assert!(syn.prop_setup, "prop_setup should survive OpenTagEnd");
assert!(
syn.prop_lang.is_some(),
"prop_lang should survive OpenTagEnd"
);
feed(&mut syn, &events[split..], &ctx);
assert!(!syn.prop_setup, "prop_setup should be reset after close");
assert!(
syn.prop_lang.is_none(),
"prop_lang should be reset after close"
);
let node = syn
.script_setup_node
.as_ref()
.expect("script_setup_node should exist");
assert!(node.is_setup);
assert_eq!(node.lang, Some(ScriptLanguage::TypeScript));
}
#[test]
fn style_scoped_module_preserved_until_close() {
let input = "<style scoped module>.a{}</style>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
let events = tokenize_events(input);
let split = events
.iter()
.position(|e| matches!(e, TokenizerEvent::OpenTagEnd { .. }))
.unwrap()
+ 1;
feed(&mut syn, &events[..split], &ctx);
assert!(syn.prop_scoped, "prop_scoped should survive OpenTagEnd");
assert!(syn.prop_module, "prop_module should survive OpenTagEnd");
feed(&mut syn, &events[split..], &ctx);
assert!(!syn.prop_scoped, "prop_scoped should be reset after close");
assert!(!syn.prop_module, "prop_module should be reset after close");
assert_eq!(syn.style_nodes.len(), 1);
assert!(syn.style_nodes[0].scoped);
assert!(syn.style_nodes[0].module);
assert!(syn.has_style_scope);
assert!(syn.has_style_module);
}
#[test]
fn multiple_style_nodes() {
let input = "<style>.a{}</style><style scoped>.b{}</style>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert_eq!(syn.style_nodes.len(), 2);
assert!(!syn.style_nodes[0].scoped);
assert!(syn.style_nodes[1].scoped);
}
#[test]
fn template_with_interpolation_and_comment() {
let input = "<template>{{ msg }}<!-- comment --></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist");
let content = ast.root.content.as_ref().unwrap();
assert_eq!(content.children.len(), 2);
let interp = &ast.nodes[content.children[0].0];
if let AstNodeKind::Interpolation(i) = &interp.kind {
assert_eq!(span_str(input, i.start, i.end), "{{ msg }}");
assert_eq!(span_str(input, i.inner_start, i.inner_end).trim(), "msg");
} else {
panic!("expected Interpolation, got {:?}", interp.kind);
}
let comment = &ast.nodes[content.children[1].0];
if let AstNodeKind::Comment(c) = &comment.kind {
assert_eq!(
span_str(input, c.content_start, c.content_end).trim(),
"comment"
);
} else {
panic!("expected Comment, got {:?}", comment.kind);
}
}
#[test]
fn directive_with_arg_and_modifiers() {
let input = "<template><div @click.stop.prevent=\"handler\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let div = &ast.nodes[div_id.0];
if let AstNodeKind::Element(el) = &div.kind {
assert_eq!(el.props.len(), 1);
let prop = &el.props[0];
assert!(prop.is_directive);
let arg_start = prop.arg_start.unwrap();
let arg_end = prop.arg_end.unwrap();
assert_eq!(span_str(input, arg_start, arg_end), "click");
assert_eq!(prop.is_dynamic, Some(false));
assert_eq!(prop.modifiers.len(), 2);
assert_eq!(
span_str(input, prop.modifiers[0].start, prop.modifiers[0].end),
"stop"
);
assert_eq!(
span_str(input, prop.modifiers[1].start, prop.modifiers[1].end),
"prevent"
);
} else {
panic!("expected Element, got {:?}", div.kind);
}
}
#[test]
fn mismatched_close_tag_emits_diagnostic_and_preserves_stack() {
let input = "<template><div></span></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let invalid_end: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XInvalidEndTag)
.collect();
let missing_end: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XMissingEndTag)
.collect();
assert_eq!(
invalid_end.len(),
2,
"expected 2 XInvalidEndTag diagnostics, got {}",
invalid_end.len()
);
assert_eq!(
missing_end.len(),
2,
"expected 2 XMissingEndTag diagnostics, got {}",
missing_end.len()
);
assert!(syn.template_ast.is_some());
}
#[test]
fn orphan_close_tag_emits_diagnostic() {
let input = "</div>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(true);
tokenize_and_feed(&mut syn, input, &ctx);
let errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
.collect();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, CompilerErrorCode::XInvalidEndTag);
}
#[test]
fn unclosed_element_at_eof_emits_diagnostic() {
let input = "<template><div><span>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let missing_end: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XMissingEndTag)
.collect();
assert_eq!(
missing_end.len(),
3,
"expected 3 XMissingEndTag diagnostics (span, div, template), got {}",
missing_end.len()
);
assert!(syn.template_ast.is_some());
}
#[test]
fn template_mode_unclosed_at_eof() {
let input = "<div><span>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(true);
tokenize_and_feed(&mut syn, input, &ctx);
let missing_end: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XMissingEndTag)
.collect();
assert_eq!(
missing_end.len(),
2,
"expected XMissingEndTag for span and div"
);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist");
let content = ast.root.content.as_ref().unwrap();
assert_eq!(content.children.len(), 1, "div should be attached to root");
assert_eq!(content.end, input.len() as u32);
}
#[test]
fn duplicate_script_emits_diagnostic() {
let input = "<script>a</script><script>b</script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let dup: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::DuplicateScript)
.collect();
assert_eq!(
dup.len(),
1,
"expected exactly 1 DuplicateScript diagnostic"
);
let node = syn.script_node.as_ref().unwrap();
let content = node.content.as_ref().unwrap();
assert_eq!(span_str(input, content.start, content.end), "b");
}
#[test]
fn duplicate_script_setup_emits_diagnostic() {
let input = "<script setup>a</script><script setup>b</script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let dup: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::DuplicateScriptSetup)
.collect();
assert_eq!(
dup.len(),
1,
"expected exactly 1 DuplicateScriptSetup diagnostic"
);
}
#[test]
fn nested_attrs_do_not_leak_to_root() {
let input = "<custom-block><x a=\"1\"></x></custom-block>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert_eq!(syn.unknown_nodes.len(), 1);
let node = &syn.unknown_nodes[0];
assert!(
node.attributes.is_empty(),
"root node should have no attributes, but got {:?}",
node.attributes
);
}
#[test]
fn quoted_root_attr_span_correctness() {
let input = "<script lang=\"ts\"></script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let node = syn.script_node.as_ref().expect("script_node should exist");
let lang = node.lang.expect("lang should be set");
assert_eq!(lang, ScriptLanguage::TypeScript);
}
#[test]
fn no_value_attr_produces_valid_span() {
let input = "<script setup></script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let node = syn
.script_setup_node
.as_ref()
.expect("script_setup_node should exist");
assert!(node.is_setup);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
}
#[test]
fn template_mode_root_content_end_updated() {
let input = "<div>hello</div>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(true);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist");
let content = ast.root.content.as_ref().unwrap();
assert_eq!(
content.end,
input.len() as u32,
"root content end should equal input length"
);
}
#[test]
fn well_formed_sfc_no_diagnostics() {
let input = "<template><div>{{ msg }}</div></template><script setup lang=\"ts\">const msg = 'hi'</script><style scoped>.a{}</style>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"well-formed input should produce no diagnostics, got: {:?}",
syn.diagnostics
);
assert!(syn.template_ast.is_some());
assert!(syn.script_setup_node.is_some());
assert_eq!(syn.style_nodes.len(), 1);
}
#[test]
fn deeply_nested_template_elements() {
let input = "<template><div><span><a>link</a></span></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist");
let root_content = ast.root.content.as_ref().unwrap();
assert_eq!(root_content.children.len(), 1);
let div_id = root_content.children[0];
let AstNodeKind::Element(div) = &ast.nodes[div_id.0].kind else {
panic!("expected Element for div");
};
let span_id = div.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(span_el) = &ast.nodes[span_id.0].kind else {
panic!("expected Element for span");
};
let a_id = span_el.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(a_el) = &ast.nodes[a_id.0].kind else {
panic!("expected Element for a");
};
let text_id = a_el.content.as_ref().unwrap().children[0];
let AstNodeKind::Text(text) = &ast.nodes[text_id.0].kind else {
panic!("expected Text for link");
};
assert_eq!(span_str(input, text.start, text.end), "link");
assert!(ast.nodes[div_id.0].parent.is_none()); assert_eq!(ast.nodes[span_id.0].parent, Some(div_id));
assert_eq!(ast.nodes[a_id.0].parent, Some(span_id));
assert_eq!(ast.nodes[text_id.0].parent, Some(a_id));
}
#[test]
fn children_flags_through_pipeline() {
let input = "<template><div>hello {{ name }}</div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(div) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
assert!(div
.children_flag
.has(crate::ast::types::ChildrenFlags::HasText));
assert!(div
.children_flag
.has(crate::ast::types::ChildrenFlags::HasInterpolation));
assert!(div.children_flag.is_text_only());
assert!(div.children_flag.has_dynamic());
assert!(!div.children_flag.needs_array());
}
#[test]
fn children_flags_mixed_element_and_text() {
let input = "<template><div>text<span></span></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(div) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
assert!(div
.children_flag
.has(crate::ast::types::ChildrenFlags::HasText));
assert!(div
.children_flag
.has(crate::ast::types::ChildrenFlags::HasElement));
assert!(!div
.children_flag
.has(crate::ast::types::ChildrenFlags::SingleChild));
assert!(!div.children_flag.is_text_only());
assert!(div.children_flag.needs_array());
}
#[test]
fn children_flags_single_element_child() {
let input = "<template><div><span></span></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(div) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
assert!(div
.children_flag
.has(crate::ast::types::ChildrenFlags::HasElement));
assert!(div
.children_flag
.has(crate::ast::types::ChildrenFlags::SingleChild));
}
#[test]
fn self_closing_element_in_template() {
let input = "<template><img /><br /></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let root_content = ast.root.content.as_ref().unwrap();
assert_eq!(root_content.children.len(), 2);
for &child_id in &root_content.children {
let AstNodeKind::Element(el) = &ast.nodes[child_id.0].kind else {
panic!("expected Element");
};
assert!(el.tag_close.is_none());
assert!(el.content.is_none()); }
}
#[test]
fn empty_template() {
let input = "<template></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(syn.diagnostics.is_empty());
let ast = syn.template_ast.as_ref().unwrap();
let root_content = ast.root.content.as_ref().unwrap();
assert!(root_content.children.is_empty());
}
#[test]
fn dynamic_directive_arg() {
let input = "<template><div v-bind:[attr]=\"val\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
assert_eq!(el.props.len(), 1);
let prop = &el.props[0];
assert!(prop.is_directive);
assert_eq!(prop.is_dynamic, Some(true));
let arg_start = prop.arg_start.unwrap();
let arg_end = prop.arg_end.unwrap();
let arg_str = span_str(input, arg_start, arg_end);
let arg_name = arg_str.trim_start_matches('[').trim_end_matches(']');
assert_eq!(arg_name, "attr");
}
#[test]
fn multiple_attrs_on_element() {
let input = "<template><div id=\"app\" class=\"main\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(syn.diagnostics.is_empty());
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
assert_eq!(el.props.len(), 2);
assert!(!el.props[0].is_directive);
assert!(!el.props[1].is_directive);
}
#[test]
fn template_mode_mixed_content() {
let input = "hello <span>world</span> {{ name }}";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(true);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let root_content = ast.root.content.as_ref().unwrap();
assert!(
root_content.children.len() >= 3,
"expected at least 3 root children, got {}",
root_content.children.len()
);
let has_text = root_content
.children
.iter()
.any(|id| matches!(ast.nodes[id.0].kind, AstNodeKind::Text(_)));
let has_element = root_content
.children
.iter()
.any(|id| matches!(ast.nodes[id.0].kind, AstNodeKind::Element(_)));
let has_interpolation = root_content
.children
.iter()
.any(|id| matches!(ast.nodes[id.0].kind, AstNodeKind::Interpolation(_)));
assert!(has_text, "should have text node");
assert!(has_element, "should have element node");
assert!(has_interpolation, "should have interpolation node");
}
#[test]
fn sibling_navigation_through_pipeline() {
let input = "<template><a></a><b></b><c></c></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let root_content = ast.root.content.as_ref().unwrap();
assert_eq!(root_content.children.len(), 3);
let a = root_content.children[0];
let b = root_content.children[1];
let c = root_content.children[2];
assert_eq!(ast.prev_sibling(a), None);
assert_eq!(ast.next_sibling(a), Some(b));
assert_eq!(ast.prev_sibling(b), Some(a));
assert_eq!(ast.next_sibling(b), Some(c));
assert_eq!(ast.prev_sibling(c), Some(b));
assert_eq!(ast.next_sibling(c), None);
}
#[test]
fn style_lang_variants() {
for (lang_val, expected) in [
("css", StyleLang::Css),
("less", StyleLang::Less),
("sass", StyleLang::Sass),
("stylus", StyleLang::Stylus),
("xyz", StyleLang::Unknown),
] {
let input = format!("<style lang=\"{}\"></style>", lang_val);
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(&input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, &input, &ctx);
assert_eq!(syn.style_nodes.len(), 1, "failed for lang={}", lang_val);
assert_eq!(
syn.style_nodes[0].lang,
Some(expected),
"wrong lang for '{}'",
lang_val
);
}
}
#[test]
fn script_lang_variants() {
let input = "<script lang=\"tsx\"></script>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let node = syn.script_node.as_ref().expect("script_node should exist");
assert_eq!(node.lang, Some(ScriptLanguage::TSX));
}
#[test]
fn multiple_unknown_root_nodes() {
let input = "<i18n>data</i18n><docs>info</docs>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert_eq!(syn.unknown_nodes.len(), 2);
let c0 = syn.unknown_nodes[0].content.as_ref().unwrap();
let c1 = syn.unknown_nodes[1].content.as_ref().unwrap();
assert_eq!(span_str(input, c0.start, c0.end), "data");
assert_eq!(span_str(input, c1.start, c1.end), "info");
}
#[test]
fn complete_sfc_all_sections() {
let input = "<template><div>hi</div></template><script>export default {}</script><script setup lang=\"ts\">const x = 1</script><style scoped>.a{}</style><style module>.b{}</style><i18n>locale</i18n>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().expect("template_ast");
assert!(!ast.root.content.as_ref().unwrap().children.is_empty());
let script = syn.script_node.as_ref().expect("script_node");
assert!(!script.is_setup);
let script_content = script.content.as_ref().unwrap();
assert_eq!(
span_str(input, script_content.start, script_content.end),
"export default {}"
);
let setup = syn.script_setup_node.as_ref().expect("script_setup_node");
assert!(setup.is_setup);
assert_eq!(setup.lang, Some(ScriptLanguage::TypeScript));
assert_eq!(syn.style_nodes.len(), 2);
assert!(syn.style_nodes[0].scoped);
assert!(!syn.style_nodes[0].module);
assert!(!syn.style_nodes[1].scoped);
assert!(syn.style_nodes[1].module);
assert!(syn.has_style_scope);
assert!(syn.has_style_module);
assert_eq!(syn.unknown_nodes.len(), 1);
}
#[test]
fn dfs_through_pipeline() {
let input = "<template><div><span>text</span></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let mut visited_kinds = Vec::new();
ast.dfs(div_id, |_id, node| {
visited_kinds.push(std::mem::discriminant(&node.kind));
});
assert_eq!(visited_kinds.len(), 3);
assert_eq!(
visited_kinds[0],
std::mem::discriminant(&AstNodeKind::Element(Box::new(
crate::ast::types::ElementNode {
tag_open: crate::types::NodeTag {
start: 0,
end: 0,
name_end: 0
},
tag_close: None,
props: Vec::new(),
content: None,
v_condition: None,
v_for: None,
v_slot: None,
v_once: None,
v_ref: None,
tag_type: crate::ast::types::TagType::Element,
is_self_closing: false,
prop_flag: crate::ast::types::PropFlag::empty(),
children_flag: crate::ast::types::ChildrenFlag::empty(),
children_mode: crate::ast::types::ChildrenMode::Empty,
is_fully_static: false,
}
)))
);
assert_eq!(
visited_kinds[2],
std::mem::discriminant(&AstNodeKind::Text(crate::ast::types::TextNode {
start: 0,
end: 0,
is_entity: false
}))
);
}
#[test]
fn directive_cache_v_if() {
let input = "<template><div v-if=\"show\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
let cond = el
.v_condition
.as_ref()
.expect("v_condition should be cached");
assert_eq!(cond.kind, crate::ast::types::ElementNodeConditionKind::If);
assert!(cond.prop.is_directive);
}
#[test]
fn directive_cache_v_else_if() {
let input = "<template><div v-else-if=\"x\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
let cond = el
.v_condition
.as_ref()
.expect("v_condition should be cached");
assert_eq!(
cond.kind,
crate::ast::types::ElementNodeConditionKind::ElseIf
);
}
#[test]
fn directive_cache_v_else() {
let input = "<template><div v-else></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
let cond = el
.v_condition
.as_ref()
.expect("v_condition should be cached");
assert_eq!(cond.kind, crate::ast::types::ElementNodeConditionKind::Else);
}
#[test]
fn directive_cache_v_for() {
let input = "<template><div v-for=\"item in items\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
let vfor = el.v_for.as_ref().expect("v_for should be cached");
assert!(vfor.is_directive);
assert_eq!(span_str(input, vfor.start, vfor.name_end), "v-for");
}
#[test]
fn directive_cache_v_slot() {
let input = "<template><Comp v-slot:default=\"{ item }\"></Comp></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let comp_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[comp_id.0].kind else {
panic!("expected Element");
};
let vslot = el.v_slot.as_ref().expect("v_slot should be cached");
assert!(vslot.is_directive);
}
#[test]
fn directive_cache_v_slot_shorthand() {
let input = "<template><Comp #default=\"{ item }\"></Comp></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let comp_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[comp_id.0].kind else {
panic!("expected Element");
};
let vslot = el
.v_slot
.as_ref()
.expect("v_slot should be cached for # shorthand");
assert!(vslot.is_directive);
}
#[test]
fn directive_cache_v_once() {
let input = "<template><div v-once></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
assert!(el.v_once.is_some(), "v_once should be set");
}
#[test]
fn duplicate_v_if_emits_warning() {
let input = "<template><div v-if=\"a\" v-if=\"b\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let dup_warnings: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XDuplicateDirective)
.collect();
assert_eq!(
dup_warnings.len(),
1,
"expected 1 duplicate directive warning, got {:?}",
syn.diagnostics
);
assert_eq!(
dup_warnings[0].severity,
crate::diagnostics::DiagnosticSeverity::Warning
);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
let cond = el
.v_condition
.as_ref()
.expect("v_condition should be cached");
assert_eq!(cond.kind, crate::ast::types::ElementNodeConditionKind::If);
}
#[test]
fn duplicate_v_for_emits_warning() {
let input = "<template><div v-for=\"a in b\" v-for=\"c in d\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let dup_warnings: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XDuplicateDirective)
.collect();
assert_eq!(
dup_warnings.len(),
1,
"expected 1 duplicate directive warning"
);
assert_eq!(
dup_warnings[0].severity,
crate::diagnostics::DiagnosticSeverity::Warning
);
}
#[test]
fn duplicate_v_slot_emits_warning() {
let input = "<template><Comp v-slot:a=\"x\" v-slot:b=\"y\"></Comp></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let dup_warnings: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XDuplicateDirective)
.collect();
assert_eq!(
dup_warnings.len(),
1,
"expected 1 duplicate directive warning"
);
}
#[test]
fn duplicate_v_once_emits_warning() {
let input = "<template><div v-once v-once></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let dup_warnings: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XDuplicateDirective)
.collect();
assert_eq!(
dup_warnings.len(),
1,
"expected 1 duplicate directive warning"
);
assert!(syn.template_ast.as_ref().unwrap().nodes.iter().any(|n| {
if let AstNodeKind::Element(el) = &n.kind {
el.v_once.is_some()
} else {
false
}
}));
}
#[test]
fn non_cached_directives_leave_fields_none() {
let input = "<template><div v-show=\"x\" v-bind:id=\"y\" @click=\"z\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
assert!(el.v_condition.is_none());
assert!(el.v_for.is_none());
assert!(el.v_slot.is_none());
assert!(el.v_once.is_none());
assert_eq!(el.props.len(), 3);
}
#[test]
fn cached_directives_not_in_props() {
let input = "<template><div v-if=\"a\" v-for=\"b in c\" v-once></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
assert_eq!(el.props.len(), 0);
assert!(el.v_condition.is_some());
assert!(el.v_for.is_some());
assert!(el.v_once.is_some());
}
#[test]
fn children_flags_auto_derive_from_cached_directives() {
let input =
"<template><div><span v-if=\"x\"></span><p v-for=\"i in list\"></p></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(
syn.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
syn.diagnostics
);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(div) = &ast.nodes[div_id.0].kind else {
panic!("expected Element");
};
assert!(
div.children_flag
.has(crate::ast::types::ChildrenFlags::HasVIf),
"parent should have HasVIf from child's cached v_condition"
);
assert!(
div.children_flag
.has(crate::ast::types::ChildrenFlags::HasVFor),
"parent should have HasVFor from child's cached v_for"
);
}
#[test]
fn prop_flag_dynamic_key() {
let input = "<template><div :key=\"id\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasDynamicKey));
}
#[test]
fn prop_flag_dynamic_class() {
let input = "<template><div :class=\"cls\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasDynamicClass));
}
#[test]
fn prop_flag_dynamic_style() {
let input = "<template><div :style=\"s\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasDynamicStyle));
}
#[test]
fn prop_flag_ref() {
let input = "<template><div ref=\"el\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
assert!(el.prop_flag.has(crate::ast::types::PropFlags::HasRef));
}
#[test]
fn prop_flag_event_listener() {
let input = "<template><div @click=\"handler\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasEventListener));
}
#[test]
fn prop_flag_custom_directive() {
let input = "<template><div v-focus></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasCustomDirective));
}
#[test]
fn prop_flag_v_show_not_custom() {
let input = "<template><div v-show=\"x\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
assert!(!el
.prop_flag
.has(crate::ast::types::PropFlags::HasCustomDirective));
}
#[test]
fn prop_flag_empty_for_static_attrs() {
let input = "<template><div id=\"app\" title=\"hello\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
assert!(el.prop_flag.is_empty());
}
#[test]
fn children_flag_has_child_with_v_slot() {
let input = "<template><Comp><template v-slot:default>hi</template></Comp></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let comp_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(comp) = &ast.nodes[comp_id.0].kind else {
panic!("expected Element")
};
assert!(comp
.children_flag
.has(crate::ast::types::ChildrenFlags::HasChildWithVSlot));
}
#[test]
fn children_flag_has_child_with_key() {
let input = "<template><div><span :key=\"id\"></span></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(div) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
assert!(div
.children_flag
.has(crate::ast::types::ChildrenFlags::HasChildWithKey));
}
#[test]
fn v_else_valid_adjacent_v_if() {
let input = "<template><div v-if=\"a\"></div><div v-else></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let else_errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XVElseNoAdjacentIf)
.collect();
assert!(
else_errors.is_empty(),
"valid v-if → v-else should not emit XVElseNoAdjacentIf, got: {:?}",
syn.diagnostics
);
}
#[test]
fn v_else_valid_with_comment_between() {
let input = "<template><div v-if=\"a\"></div><!-- comment --><div v-else></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let else_errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XVElseNoAdjacentIf)
.collect();
assert!(
else_errors.is_empty(),
"comment between v-if and v-else should be valid"
);
}
#[test]
fn v_else_if_valid_with_whitespace_between() {
let input = "<template><div v-if=\"a\"></div> <div v-else-if=\"b\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let else_errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XVElseNoAdjacentIf)
.collect();
assert!(
else_errors.is_empty(),
"whitespace between v-if and v-else-if should be valid"
);
}
#[test]
fn v_else_valid_full_chain() {
let input =
"<template><div v-if=\"a\"></div><div v-else-if=\"b\"></div><div v-else></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let else_errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XVElseNoAdjacentIf)
.collect();
assert!(else_errors.is_empty(), "full v-if chain should be valid");
}
#[test]
fn v_else_invalid_alone() {
let input = "<template><div v-else></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let else_errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XVElseNoAdjacentIf)
.collect();
assert_eq!(
else_errors.len(),
1,
"v-else alone should emit XVElseNoAdjacentIf"
);
}
#[test]
fn v_else_invalid_after_plain_element() {
let input = "<template><span></span><div v-else></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let else_errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XVElseNoAdjacentIf)
.collect();
assert_eq!(
else_errors.len(),
1,
"v-else after plain element should emit XVElseNoAdjacentIf"
);
}
#[test]
fn v_else_invalid_after_v_for() {
let input = "<template><div v-for=\"x in y\"></div><div v-else></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let else_errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XVElseNoAdjacentIf)
.collect();
assert_eq!(
else_errors.len(),
1,
"v-else after v-for should emit XVElseNoAdjacentIf"
);
}
#[test]
fn v_else_invalid_after_v_else() {
let input = "<template><div v-if=\"a\"></div><div v-else></div><div v-else></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let else_errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XVElseNoAdjacentIf)
.collect();
assert_eq!(
else_errors.len(),
1,
"v-else after v-else should emit XVElseNoAdjacentIf, got: {:?}",
syn.diagnostics
);
}
#[test]
fn v_if_alone_no_adjacency_error() {
let input = "<template><div v-if=\"a\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let else_errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::XVElseNoAdjacentIf)
.collect();
assert!(
else_errors.is_empty(),
"v-if alone should not emit adjacency error"
);
}
#[test]
fn tag_type_html_element() {
let input = "<template><div></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert_eq!(el.tag_type, crate::ast::types::TagType::Element);
}
#[test]
fn tag_type_pascal_case_component() {
let input = "<template><MyComp></MyComp></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert_eq!(el.tag_type, crate::ast::types::TagType::Component);
}
#[test]
fn tag_type_dash_case_component() {
let input = "<template><my-comp></my-comp></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert_eq!(el.tag_type, crate::ast::types::TagType::Component);
}
#[test]
fn tag_type_unknown_lowercase_component() {
let input = "<template><foobar></foobar></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert_eq!(el.tag_type, crate::ast::types::TagType::Component);
}
#[test]
fn tag_type_slot_outlet() {
let input = "<template><slot></slot></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert_eq!(el.tag_type, crate::ast::types::TagType::SlotOutlet);
}
#[test]
fn tag_type_template_wrapper() {
let input = "<template><div><template v-if=\"x\">hi</template></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(div) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
let tmpl_id = div.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(tmpl) = &ast.nodes[tmpl_id.0].kind else {
panic!("expected Element")
};
assert_eq!(tmpl.tag_type, crate::ast::types::TagType::Template);
}
#[test]
fn tag_type_svg_element() {
let input = "<template><svg></svg></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert_eq!(el.tag_type, crate::ast::types::TagType::Element);
}
#[test]
fn is_self_closing_true() {
let input = "<template><br /></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el.is_self_closing);
}
#[test]
fn is_self_closing_false() {
let input = "<template><div></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(!el.is_self_closing);
}
#[test]
fn prop_flag_has_model() {
let input = "<template><input v-model=\"val\" /></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el.prop_flag.has(crate::ast::types::PropFlags::HasModel));
}
#[test]
fn prop_flag_has_show() {
let input = "<template><div v-show=\"x\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el.prop_flag.has(crate::ast::types::PropFlags::HasShow));
}
#[test]
fn prop_flag_has_v_html() {
let input = "<template><div v-html=\"content\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el.prop_flag.has(crate::ast::types::PropFlags::HasVHtml));
}
#[test]
fn prop_flag_has_v_text() {
let input = "<template><div v-text=\"msg\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el.prop_flag.has(crate::ast::types::PropFlags::HasVText));
}
#[test]
fn prop_flag_has_static_class() {
let input = "<template><div class=\"foo\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasStaticClass));
}
#[test]
fn prop_flag_has_static_style() {
let input = "<template><div style=\"color:red\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasStaticStyle));
}
#[test]
fn prop_flag_has_bind_spread() {
let input = "<template><div v-bind=\"obj\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasBindSpread));
}
#[test]
fn prop_flag_has_on_spread() {
let input = "<template><div v-on=\"handlers\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el.prop_flag.has(crate::ast::types::PropFlags::HasOnSpread));
}
#[test]
fn prop_flag_merge_class() {
let input = "<template><div class=\"a\" :class=\"b\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasStaticClass));
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasDynamicClass));
}
#[test]
fn prop_flag_merge_style() {
let input = "<template><div style=\"color:red\" :style=\"s\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[id.0].kind else {
panic!("expected Element")
};
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasStaticStyle));
assert!(el
.prop_flag
.has(crate::ast::types::PropFlags::HasDynamicStyle));
}
#[test]
fn template_element_attr_value_end() {
let input = "<template><div id=\"app\" :class=\"cls\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
let id_prop = &el.props[0];
let vs = id_prop.value_start.expect("value_start should be set");
let ve = id_prop
.value_end
.expect("value_end should be set for quoted attr");
assert_eq!(span_str(input, vs, ve), "app");
let class_prop = &el.props[1];
let vs = class_prop.value_start.expect("value_start should be set");
let ve = class_prop
.value_end
.expect("value_end should be set for quoted directive");
assert_eq!(span_str(input, vs, ve), "cls");
}
#[test]
fn template_element_attr_no_value_end() {
let input = "<template><div v-once></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element")
};
let v_once = el.v_once.as_ref().expect("v_once should be cached");
assert!(v_once.value_start.is_none());
assert!(v_once.value_end.is_none());
}
#[test]
fn element_with_v_if_and_v_for_both_cached() {
let input = r#"<template><div><span v-if="ok" v-for="i in list"></span></div></template>"#;
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast.as_ref().unwrap();
let div_id = ast.root.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(div_el) = &ast.nodes[div_id.0].kind else {
panic!("expected Element for div");
};
let span_id = div_el.content.as_ref().unwrap().children[0];
let AstNodeKind::Element(span_el) = &ast.nodes[span_id.0].kind else {
panic!("expected Element for span");
};
assert!(
span_el.v_condition.is_some(),
"v-if should be cached on span"
);
assert!(span_el.v_for.is_some(), "v-for should be cached on span");
assert!(div_el
.children_flag
.has(crate::ast::types::ChildrenFlags::HasVIf));
assert!(div_el
.children_flag
.has(crate::ast::types::ChildrenFlags::HasVFor));
}
#[test]
fn syntax_public_getters() {
let input = r#"<script setup lang="ts">const x = 1;</script>
<template><div>hello</div></template>
<style scoped lang="scss">.foo{}</style>
<custom-block>data</custom-block>"#;
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
assert!(syn.script().is_none(), "no plain script block");
assert!(syn.script_setup().is_some(), "script setup should exist");
assert_eq!(syn.style_nodes().len(), 1);
assert!(syn.style_nodes()[0].scoped);
assert!(syn.has_style_scope());
assert!(!syn.has_style_module());
assert!(!syn.is_vapor());
assert!(syn.template_ast().is_some());
assert_eq!(syn.unknown_nodes().len(), 1);
}
#[test]
fn ref_attribute_cached_in_v_ref() {
let input = r#"<div ref="myRef" class="foo"></div>"#;
let options = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &options);
let mut syn = Syntax::new(true); tokenize_and_feed(&mut syn, input, &ctx);
let ast = syn.template_ast().expect("should have template AST");
let root_children = ast.root.content.as_ref().unwrap();
assert_eq!(root_children.children.len(), 1);
let node = &ast.nodes[root_children.children[0].0];
let AstNodeKind::Element(el) = &node.kind else {
panic!("expected Element");
};
assert!(el.v_ref.is_some(), "v_ref should be cached on the element");
let ref_prop = el.v_ref.as_ref().unwrap();
let ref_value = span_str(
input,
ref_prop.value_start.unwrap(),
ref_prop.value_end.unwrap(),
);
assert_eq!(ref_value, "myRef");
for prop in &el.props {
let prop_name = span_str(input, prop.start, prop.name_end);
assert_ne!(prop_name, "ref", "ref should not be in element.props");
}
assert!(
el.props.iter().any(|p| {
let n = span_str(input, p.start, p.name_end);
n == "class"
}),
"class should remain in element.props"
);
assert!(
el.prop_flag.has(crate::ast::types::PropFlags::HasRef),
"HasRef prop flag should still be set"
);
}
#[test]
fn void_element_img_no_close_tag() {
let input =
"<template><div><img src=\"test.png\" alt=\"test\"><span>text</span></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Void <img> should not cause errors: {:?}",
errors
);
let ast = syn
.template_ast
.as_ref()
.expect("template_ast should exist");
let root_children = ast.root.content.as_ref().unwrap().children.as_slice();
assert_eq!(root_children.len(), 1, "root should have 1 child (div)");
let div = &ast.nodes[root_children[0].0];
if let AstNodeKind::Element(el) = &div.kind {
let content = el.content.as_ref().expect("div should have content");
assert_eq!(
content.children.len(),
2,
"div should have 2 children (img + span)"
);
} else {
panic!("Expected element node for div");
}
}
#[test]
fn void_element_br_and_hr() {
let input = "<template><p>text<br>more<hr>end</p></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Void <br> and <hr> should not cause errors: {:?}",
errors
);
}
#[test]
fn void_element_input_with_attrs() {
let input = "<template><form><input type=\"text\" v-model=\"name\"><button>Submit</button></form></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Void <input> should not cause errors: {:?}",
errors
);
}
#[test]
fn void_element_explicit_close_tag_tolerated() {
let input = "<template><div><img src=\"test.png\"></img></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Explicit </img> should be tolerated: {:?}",
errors
);
}
#[test]
fn void_element_self_closing_still_works() {
let input = "<template><img src=\"test.png\" /></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_and_feed(&mut syn, input, &ctx);
let errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Self-closing <img /> should still work: {:?}",
errors
);
}
#[test]
fn sfc_mode_custom_block_html_like_content_no_errors() {
let input = "<docs>\n## Title\n\nDefault to `@`, `Array<string>` also supported.\n</docs>\n<template><div>hi</div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Custom block with HTML-like content should not produce errors: {:?}",
errors
);
assert_eq!(syn.unknown_nodes.len(), 1);
let content = syn.unknown_nodes[0].content.as_ref().unwrap();
let text = span_str(input, content.start, content.end);
assert!(
text.contains("Array<string>"),
"Content should contain raw text including HTML-like tokens: {}",
text
);
let ast = syn.template_ast().expect("template AST should exist");
assert!(ast.root.content.is_some());
}
#[test]
fn sfc_mode_custom_block_component_inside_template_not_affected() {
let input = "<template><docs>inner content</docs></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Component inside template should work normally: {:?}",
errors
);
assert_eq!(
syn.unknown_nodes.len(),
0,
"docs inside template is a component, not a root block"
);
}
#[test]
fn sfc_mode_multiple_custom_blocks_with_html_content() {
let input = "<i18n>{\"key\": \"<b>value</b>\"}</i18n>\n<docs>Array<T> is generic</docs>\n<template><div/></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let errors: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Multiple custom blocks with HTML-like content should not produce errors: {:?}",
errors
);
assert_eq!(syn.unknown_nodes.len(), 2);
}
#[test]
fn tokenizer_error_eof_in_comment() {
let input = "<template><!-- unclosed comment</template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let errs: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::EofInComment)
.collect();
assert_eq!(errs.len(), 1, "should emit EofInComment diagnostic");
assert_eq!(
errs[0].severity,
crate::diagnostics::DiagnosticSeverity::Error
);
assert!(
!syn.diagnostics
.iter()
.any(|d| d.code == CompilerErrorCode::EofInTag),
"should not emit EofInTag for unclosed comment"
);
}
#[test]
fn tokenizer_error_abrupt_closing_of_empty_comment_short() {
let input = "<template><!-->text</template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let warns: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::AbruptClosingOfEmptyComment)
.collect();
assert_eq!(
warns.len(),
1,
"should emit AbruptClosingOfEmptyComment for <!-->",
);
assert_eq!(
warns[0].severity,
crate::diagnostics::DiagnosticSeverity::Warning
);
assert!(
!syn.diagnostics
.iter()
.any(|d| d.code == CompilerErrorCode::EofInComment),
"should not emit EofInComment for abrupt close"
);
}
#[test]
fn tokenizer_error_abrupt_closing_of_empty_comment_long() {
let input = "<template><!--->text</template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let warns: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::AbruptClosingOfEmptyComment)
.collect();
assert_eq!(
warns.len(),
1,
"should emit AbruptClosingOfEmptyComment for <!--->",
);
assert_eq!(
warns[0].severity,
crate::diagnostics::DiagnosticSeverity::Warning
);
}
#[test]
fn tokenizer_error_incorrectly_opened_comment_declaration() {
let input = "<template><!something></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let warns: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::IncorrectlyOpenedComment)
.collect();
assert_eq!(
warns.len(),
1,
"should emit IncorrectlyOpenedComment for <!something>"
);
assert_eq!(
warns[0].severity,
crate::diagnostics::DiagnosticSeverity::Warning
);
}
#[test]
fn tokenizer_error_incorrectly_opened_comment_single_dash() {
let input = "<template><!-x></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let warns: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::IncorrectlyOpenedComment)
.collect();
assert_eq!(
warns.len(),
1,
"should emit IncorrectlyOpenedComment for <!-x>"
);
assert_eq!(
warns[0].severity,
crate::diagnostics::DiagnosticSeverity::Warning
);
}
#[test]
fn tokenizer_error_cdata_in_html_content() {
let input = "<template><div><![CDATA[text]]></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let warns: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::CdataInHtmlContent)
.collect();
assert_eq!(
warns.len(),
1,
"should emit CdataInHtmlContent for <![CDATA[ in HTML"
);
assert_eq!(
warns[0].severity,
crate::diagnostics::DiagnosticSeverity::Warning
);
}
#[test]
fn tokenizer_error_eof_in_cdata() {
let input = "<template><![CDATA[unclosed";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let errs: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::EofInCdata)
.collect();
assert_eq!(errs.len(), 1, "should emit EofInCdata diagnostic");
assert_eq!(
errs[0].severity,
crate::diagnostics::DiagnosticSeverity::Error
);
}
#[test]
fn tokenizer_error_unexpected_equals_sign_before_attribute_name() {
let input = "<template><div =\"val\"></div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let warns: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::UnexpectedEqualsSignBeforeAttributeName)
.collect();
assert!(
!warns.is_empty(),
"should emit UnexpectedEqualsSignBeforeAttributeName"
);
assert_eq!(
warns[0].severity,
crate::diagnostics::DiagnosticSeverity::Warning
);
}
#[test]
fn tokenizer_error_unexpected_question_mark_instead_of_tag_name() {
let input = "<template><?xml version=\"1.0\"?></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let warns: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| d.code == CompilerErrorCode::UnexpectedQuestionMarkInsteadOfTagName)
.collect();
assert_eq!(
warns.len(),
1,
"should emit UnexpectedQuestionMarkInsteadOfTagName for <?xml>"
);
assert_eq!(
warns[0].severity,
crate::diagnostics::DiagnosticSeverity::Warning
);
}
#[test]
fn tokenizer_no_invalid_first_char_for_text_with_less_than() {
let input = "<template>2 < 1</template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
assert!(
!syn.diagnostics
.iter()
.any(|d| d.code == CompilerErrorCode::InvalidFirstCharacterOfTagName),
"should NOT emit InvalidFirstCharacterOfTagName for text containing '<'"
);
}
#[test]
fn tokenizer_no_spurious_diagnostics_for_valid_template() {
let input = "<template><div class=\"foo\"><!-- valid comment -->text</div></template>";
let opts = SyntaxPluginOptions::default();
let ctx = make_ctx(input, &opts);
let mut syn = Syntax::new(false);
tokenize_sfc_and_feed(&mut syn, input, &ctx);
let new_codes = [
CompilerErrorCode::EofInComment,
CompilerErrorCode::EofInCdata,
CompilerErrorCode::AbruptClosingOfEmptyComment,
CompilerErrorCode::IncorrectlyOpenedComment,
CompilerErrorCode::CdataInHtmlContent,
CompilerErrorCode::UnexpectedEqualsSignBeforeAttributeName,
CompilerErrorCode::UnexpectedQuestionMarkInsteadOfTagName,
];
let spurious: Vec<_> = syn
.diagnostics
.iter()
.filter(|d| new_codes.contains(&d.code))
.collect();
assert!(
spurious.is_empty(),
"valid template should not emit any new tokenizer error diagnostics, got: {:?}",
spurious
);
}
#[test]
fn is_member_expression_accepts_ts_as_cast() {
assert!(super::is_member_expression("expanded as string[]"));
assert!(super::is_member_expression(
"form.value as Record<string, any>"
));
assert!(super::is_member_expression("items as unknown"));
assert!(super::is_member_expression("expanded"));
assert!(super::is_member_expression("form.value"));
assert!(super::is_member_expression("obj['key']"));
assert!(super::is_member_expression("obj?.nested"));
assert!(!super::is_member_expression("a + b"));
assert!(!super::is_member_expression("fn()"));
assert!(!super::is_member_expression(""));
}
#[test]
fn v_slot_dotted_name_includes_full_name_in_arg() {
let input = r#"<template><Comp><template v-slot:item.title="{ val }"><span>{{ val }}</span></template></Comp></template>"#;
let parsed = crate::compile::parse_sfc(input, None, None);
let ast = parsed.template_ast().expect("template AST");
let comp_node = &ast.nodes[ast.root.content.as_ref().unwrap().children[0].0];
let comp_el = match &comp_node.kind {
AstNodeKind::Element(el) => el,
_ => panic!("expected element"),
};
let inner_id = comp_el.content.as_ref().unwrap().children[0];
let inner_node = &ast.nodes[inner_id.0];
let inner_el = match &inner_node.kind {
AstNodeKind::Element(el) => el,
_ => panic!("expected element"),
};
let v_slot = inner_el.v_slot.as_ref().expect("v_slot should exist");
let arg_start = v_slot.arg_start.expect("arg_start");
let arg_end = v_slot.arg_end.expect("arg_end");
let slot_name = &input[arg_start as usize..arg_end as usize];
assert_eq!(
slot_name, "item.title",
"v-slot arg should include the full dotted name"
);
assert!(
v_slot.modifiers.is_empty(),
"v-slot modifiers should be empty after dot merging, got: {:?}",
v_slot.modifiers
);
assert!(
!parsed.has_errors(),
"should have no errors for dotted slot names"
);
}
#[test]
fn v_slot_shorthand_dotted_name() {
let input =
r#"<template><Comp><template #item.title="{ val }"><span/></template></Comp></template>"#;
let parsed = crate::compile::parse_sfc(input, None, None);
let ast = parsed.template_ast().expect("template AST");
let comp_node = &ast.nodes[ast.root.content.as_ref().unwrap().children[0].0];
let comp_el = match &comp_node.kind {
AstNodeKind::Element(el) => el,
_ => panic!("expected element"),
};
let inner_id = comp_el.content.as_ref().unwrap().children[0];
let inner_node = &ast.nodes[inner_id.0];
let inner_el = match &inner_node.kind {
AstNodeKind::Element(el) => el,
_ => panic!("expected element"),
};
let v_slot = inner_el.v_slot.as_ref().expect("v_slot should exist");
let arg_start = v_slot.arg_start.expect("arg_start");
let arg_end = v_slot.arg_end.expect("arg_end");
let slot_name = &input[arg_start as usize..arg_end as usize];
assert_eq!(
slot_name, "item.title",
"#item.title shorthand should include full dotted name"
);
assert!(
v_slot.modifiers.is_empty(),
"shorthand v-slot modifiers should be empty"
);
}