use crate::ast::builder::TemplateAstBuilder;
use crate::ast::types::{
AstNodeKind, ElementNode, ElementNodeCondition, ElementNodeConditionKind, PropFlags, TagType,
TemplateAst,
};
use crate::common::{ErrorCode, Span};
use crate::cursor::ScriptLanguage;
use crate::diagnostics::{CompilerErrorCode, Diagnostic, SyntaxPluginContext};
use crate::parser::types::{
RootNodeKind, RootNodeScript, RootNodeStyle, RootNodeTemplate, RootNodeTemplateContent,
RootNodeUnknown, StyleLang,
};
use crate::tokenizer::{Event as TokenizerEvent, QuoteType};
use crate::types::{NodeId, NodeProp, NodeTag};
use crate::utils::vue::{is_html_tag, is_mathml_tag, is_svg_tag, is_void_tag};
use rustc_hash::FxHashSet;
use smallvec::SmallVec;
#[cfg(test)]
mod mod_tests;
pub mod types;
#[derive(Debug, Clone, Copy)]
struct StackElement {
tag_open_start: u32,
tag_open_end: u32,
name_start: u32,
name_end: u32,
}
impl StackElement {
fn name_bytes<'a>(&self, ctx: &SyntaxPluginContext<'a>) -> &'a [u8] {
&ctx.bytes[self.name_start as usize..self.name_end as usize]
}
}
pub struct Syntax {
template_mode: bool,
has_style_scope: bool,
has_style_module: bool,
is_vapor: bool,
current_prop: Option<NodeProp>,
element_props: Vec<NodeProp>,
prop_lang: Option<Span>,
prop_src: Option<Span>,
prop_generic: Option<Span>,
prop_attrs: Option<Span>,
prop_setup: bool,
prop_scoped: bool,
prop_module: bool,
stack_elements: Vec<StackElement>,
script_node: Option<RootNodeScript>,
script_setup_node: Option<RootNodeScript>,
style_nodes: Vec<RootNodeStyle>,
unknown_nodes: Vec<RootNodeUnknown>,
ast_builder: Option<TemplateAstBuilder>,
template_ast: Option<TemplateAst>,
seen_attr_names: FxHashSet<Vec<u8>>,
diagnostics: Vec<Diagnostic>,
}
impl Syntax {
pub fn new(template_mode: bool) -> Self {
let ast_builder = if template_mode {
let synthetic_root = RootNodeTemplate {
tag_open: NodeTag {
start: 0,
end: 0,
name_end: 0,
},
tag_close: None,
lang: None,
attributes: Vec::new(),
content: Some(RootNodeTemplateContent {
start: 0,
end: 0,
children: SmallVec::new(),
}),
};
Some(TemplateAstBuilder::new(synthetic_root))
} else {
None
};
Self {
template_mode,
script_node: None,
script_setup_node: None,
style_nodes: Vec::new(),
unknown_nodes: Vec::new(),
element_props: Vec::with_capacity(20),
has_style_scope: false,
has_style_module: false,
is_vapor: false,
current_prop: None,
prop_lang: None,
prop_src: None,
prop_generic: None,
prop_attrs: None,
prop_setup: false,
prop_scoped: false,
prop_module: false,
stack_elements: Vec::with_capacity(16),
ast_builder,
template_ast: None,
seen_attr_names: FxHashSet::default(),
diagnostics: Vec::new(),
}
}
pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.severity == crate::diagnostics::DiagnosticSeverity::Error)
}
pub fn script(&self) -> Option<&RootNodeScript> {
self.script_node.as_ref()
}
pub fn script_setup(&self) -> Option<&RootNodeScript> {
self.script_setup_node.as_ref()
}
pub fn style_nodes(&self) -> &[RootNodeStyle] {
&self.style_nodes
}
pub fn unknown_nodes(&self) -> &[RootNodeUnknown] {
&self.unknown_nodes
}
pub fn template_ast(&self) -> Option<&TemplateAst> {
self.template_ast.as_ref()
}
pub fn take_template_ast(&mut self) -> Option<TemplateAst> {
self.template_ast.take()
}
pub fn has_style_scope(&self) -> bool {
self.has_style_scope
}
pub fn has_style_module(&self) -> bool {
self.has_style_module
}
pub fn is_vapor(&self) -> bool {
self.is_vapor
}
pub fn into_parsed_sfc(mut self) -> types::ParsedSfc {
let has_errors = self.has_errors();
types::ParsedSfc {
template_ast: self.template_ast.take(),
script_node: self.script_node.take(),
script_setup_node: self.script_setup_node.take(),
style_nodes: std::mem::take(&mut self.style_nodes),
unknown_nodes: std::mem::take(&mut self.unknown_nodes),
has_style_scope: self.has_style_scope,
has_style_module: self.has_style_module,
is_vapor: self.is_vapor,
diagnostics: std::mem::take(&mut self.diagnostics),
has_errors,
}
}
}
#[inline]
fn make_open_tag(se: &StackElement) -> NodeTag {
NodeTag {
start: se.tag_open_start,
end: se.tag_open_end,
name_end: se.name_end,
}
}
#[inline]
fn make_close_tag(start: u32, end: u32, name_end: u32) -> NodeTag {
NodeTag {
start,
end,
name_end,
}
}
#[inline]
fn resolve_root_kind(name: &[u8]) -> RootNodeKind {
match name {
b"template" => RootNodeKind::Template,
b"script" => RootNodeKind::Script,
b"style" => RootNodeKind::Style,
_ => RootNodeKind::Unknown,
}
}
#[inline]
fn resolve_tag_type(name: &[u8]) -> TagType {
if name == b"slot" {
return TagType::SlotOutlet;
}
if name == b"template" {
return TagType::Template;
}
if let Some(&first) = name.first() {
if first.is_ascii_uppercase() {
return TagType::Component;
}
}
if name.contains(&b'-') {
return TagType::Component;
}
if is_html_tag(name) || is_svg_tag(name) || is_mathml_tag(name) {
return TagType::Element;
}
TagType::Component
}
impl<'alloc> Syntax {
pub fn handle(&mut self, event: &TokenizerEvent<'alloc>, ctx: &SyntaxPluginContext<'alloc>) {
match event {
TokenizerEvent::OpenTagName { start, end } => {
self.handle_tag_open(*start, *end, ctx);
}
TokenizerEvent::OpenTagEnd { end } => {
self.handle_open_tag_end(*end, ctx);
}
TokenizerEvent::SelfClosingTag { end } => {
self.handle_self_closing(*end, ctx);
}
TokenizerEvent::CloseTag {
start,
end,
name_end,
} => {
self.handle_close_tag(*start, *end, *name_end, ctx);
}
TokenizerEvent::AttribName { start, end } => {
self.handle_attribute_name(*start, *end);
}
TokenizerEvent::DirName { start, end } => {
self.handle_directive_name(*start, *end);
}
TokenizerEvent::DirArg {
is_dynamic,
start,
end,
} => {
self.handle_directive_arg(*start, *end, *is_dynamic);
}
TokenizerEvent::DirModifier { start, end } => {
self.handle_directive_modifier(*start, *end);
}
TokenizerEvent::AttribData { start, end: _ } => {
self.handle_attribute_value(*start);
}
TokenizerEvent::AttribEnd { quote, end } => {
self.handle_attribute_end(*end, *quote, ctx);
}
TokenizerEvent::Text { start, end } => {
self.handle_text_leaf(*start, *end, false);
}
TokenizerEvent::TextEntity { start, end } => {
self.handle_text_leaf(*start, *end, true);
}
TokenizerEvent::Comment {
start,
end,
content_start,
content_end,
} => {
self.handle_comment_leaf(*start, *end, *content_start, *content_end);
}
TokenizerEvent::Interpolation {
start,
end,
delimiter_open_len,
delimiter_close_len,
} => {
let inner_start = *start + *delimiter_open_len as u32;
let inner_end = *end - *delimiter_close_len as u32;
self.handle_interpolation_leaf(*start, *end, inner_start, inner_end);
}
TokenizerEvent::End => {
self.handle_end(ctx);
}
TokenizerEvent::Error { code, index } => {
self.handle_tokenizer_error(*code, *index);
}
_ => {}
}
}
}
impl Syntax {
fn handle_tag_open<'alloc>(
&mut self,
start: u32,
name_end: u32,
ctx: &SyntaxPluginContext<'alloc>,
) {
let se = StackElement {
tag_open_start: start,
tag_open_end: start, name_start: start + 1,
name_end,
};
self.stack_elements.push(se);
self.seen_attr_names.clear();
if let Some(builder) = self.ast_builder.as_mut() {
let is_sfc_root = !self.template_mode && self.stack_elements.len() == 1;
if !is_sfc_root {
let open_tag = make_open_tag(
self.stack_elements
.last()
.expect("invariant: stack non-empty after push"),
);
builder.open_element(open_tag);
let tag_name = &ctx.bytes[(start + 1) as usize..name_end as usize];
let tag_type = resolve_tag_type(tag_name);
builder.set_tag_type(tag_type);
}
}
}
fn handle_open_tag_end<'alloc>(&mut self, end: u32, ctx: &SyntaxPluginContext<'alloc>) {
if let Some(last) = self.stack_elements.last_mut() {
last.tag_open_end = end;
} else {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::EofInTag)
.with_span(Span::new(end.saturating_sub(1), end)),
);
return;
}
let is_sfc_root = !self.template_mode && self.stack_elements.len() == 1;
if is_sfc_root {
let root_event = *self
.stack_elements
.last()
.expect("invariant: stack non-empty when is_sfc_root");
let name = root_event.name_bytes(ctx);
self.handle_root_open(&root_event, name, ctx);
} else {
let se = *self
.stack_elements
.last()
.expect("invariant: stack non-empty in non-root branch");
let tag_name = &ctx.bytes[se.name_start as usize..se.name_end as usize];
let is_void = is_void_tag(tag_name);
if let Some(builder) = self.ast_builder.as_mut() {
builder.set_tag_open_end(end);
if is_void {
builder.set_self_closing();
let closed_id = builder.close_element(None, end);
self.validate_v_condition_adjacency(closed_id, ctx);
self.validate_v_if_same_key(closed_id, ctx);
} else {
builder.mark_element_content_start(end);
}
}
if is_void {
self.stack_elements.pop();
}
}
}
fn handle_self_closing<'alloc>(&mut self, end: u32, ctx: &SyntaxPluginContext<'alloc>) {
if let Some(last) = self.stack_elements.last_mut() {
last.tag_open_end = end;
}
let se = match self.stack_elements.pop() {
Some(se) => se,
None => {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::EofInTag)
.with_span(Span::new(end.saturating_sub(2), end)),
);
return;
}
};
let was_sfc_root = !self.template_mode && self.stack_elements.is_empty();
let name = se.name_bytes(ctx);
if was_sfc_root {
match resolve_root_kind(name) {
RootNodeKind::Template => {
if let Some(mut builder) = self.ast_builder.take() {
builder.ast.root.tag_close = None;
builder.ast.root.content = None;
self.template_ast = Some(builder.finish());
} else {
let root = RootNodeTemplate {
tag_open: make_open_tag(&se),
tag_close: None,
lang: self.prop_lang.take(),
attributes: self.take_props(),
content: None,
};
self.template_ast = Some(TemplateAst::new(root));
}
self.reset_prop_state();
}
RootNodeKind::Script => {
self.store_script_node(&se, None, None, ctx);
}
RootNodeKind::Style => {
self.store_style_node(&se, None, None, ctx);
}
RootNodeKind::Unknown => {
self.store_unknown_node(&se, None, None);
}
}
} else if let Some(builder) = self.ast_builder.as_mut() {
builder.set_tag_open_end(end);
builder.set_self_closing();
let closed_id = builder.close_element(None, end);
self.validate_v_condition_adjacency(closed_id, ctx);
self.validate_v_if_same_key(closed_id, ctx);
}
}
fn handle_close_tag<'alloc>(
&mut self,
start: u32,
end: u32,
name_end: u32,
ctx: &SyntaxPluginContext<'alloc>,
) {
let close_name = &ctx.bytes[(start + 2) as usize..name_end as usize];
if is_void_tag(close_name) {
return;
}
let open = match self.stack_elements.last() {
Some(se) => se,
None => {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XInvalidEndTag)
.with_span(Span::new(start, end)),
);
return;
}
};
let open_name = open.name_bytes(ctx);
if !open_name.eq_ignore_ascii_case(close_name) {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XInvalidEndTag)
.with_span(Span::new(start, end)),
);
return;
}
debug_assert!(
!self.stack_elements.is_empty(),
"stack_elements should not be empty after name match"
);
let open = self
.stack_elements
.pop()
.expect("invariant: stack non-empty after name match");
let was_sfc_root = !self.template_mode && self.stack_elements.is_empty();
let tag_close = make_close_tag(start, end, name_end);
if was_sfc_root {
let content = Some(Span::new(open.tag_open_end, start));
match resolve_root_kind(open_name) {
RootNodeKind::Template => {
if let Some(mut builder) = self.ast_builder.take() {
builder.ast.root.tag_close = Some(tag_close);
if let Some(c) = builder.ast.root.content.as_mut() {
c.end = start;
}
self.template_ast = Some(builder.finish());
}
}
RootNodeKind::Script => {
self.store_script_node(&open, Some(tag_close), content, ctx);
}
RootNodeKind::Style => {
self.store_style_node(&open, Some(tag_close), content, ctx);
}
RootNodeKind::Unknown => {
self.store_unknown_node(&open, Some(tag_close), content);
}
}
} else if let Some(builder) = self.ast_builder.as_mut() {
let closed_id = builder.close_element(Some(tag_close), start);
self.validate_v_condition_adjacency(closed_id, ctx);
self.validate_v_if_same_key(closed_id, ctx);
self.validate_slot_names(closed_id, ctx);
}
}
fn handle_end<'alloc>(&mut self, ctx: &SyntaxPluginContext<'alloc>) {
while let Some(se) = self.stack_elements.pop() {
let is_sfc_root = !self.template_mode && self.stack_elements.is_empty();
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XMissingEndTag)
.with_span(Span::new(se.tag_open_start, se.tag_open_end)),
);
if !is_sfc_root {
if let Some(builder) = self.ast_builder.as_mut() {
let closed_id = builder.close_element(None, se.tag_open_end);
self.validate_v_condition_adjacency(closed_id, ctx);
self.validate_v_if_same_key(closed_id, ctx);
}
} else {
let name = &ctx.bytes[se.name_start as usize..se.name_end as usize];
let content = Some(Span::new(se.tag_open_end, ctx.bytes.len() as u32));
match resolve_root_kind(name) {
RootNodeKind::Template => {
if let Some(mut builder) = self.ast_builder.take() {
builder.ast.root.tag_close = None;
if let Some(c) = builder.ast.root.content.as_mut() {
c.end = se.tag_open_end;
}
self.template_ast = Some(builder.finish());
}
}
RootNodeKind::Script => {
self.store_script_node(&se, None, content, ctx);
}
RootNodeKind::Style => {
self.store_style_node(&se, None, content, ctx);
}
RootNodeKind::Unknown => {
self.store_unknown_node(&se, None, content);
}
}
}
}
if let Some(mut builder) = self.ast_builder.take() {
if self.template_mode {
if let Some(c) = builder.ast.root.content.as_mut() {
c.end = ctx.bytes.len() as u32;
}
}
self.template_ast = Some(builder.finish());
}
}
}
impl Syntax {
fn handle_tokenizer_error(&mut self, code: ErrorCode, index: u32) {
let (compiler_code, severity_is_error) = match code {
ErrorCode::DUPLICATE_ATTRIBUTE => (CompilerErrorCode::DuplicateAttribute, true),
ErrorCode::EOF_BEFORE_TAG_NAME => (CompilerErrorCode::EofBeforeTagName, true),
ErrorCode::EOF_IN_CDATA => (CompilerErrorCode::EofInCdata, true),
ErrorCode::EOF_IN_COMMENT => (CompilerErrorCode::EofInComment, true),
ErrorCode::EOF_IN_TAG => (CompilerErrorCode::EofInTag, true),
ErrorCode::MISSING_ATTRIBUTE_VALUE => (CompilerErrorCode::MissingAttributeValue, true),
ErrorCode::MISSING_END_TAG_NAME => (CompilerErrorCode::MissingEndTagName, true),
ErrorCode::X_MISSING_INTERPOLATION_END => {
(CompilerErrorCode::XMissingInterpolationEnd, true)
}
ErrorCode::X_MISSING_DIRECTIVE_NAME => (CompilerErrorCode::XMissingDirectiveName, true),
ErrorCode::X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END => {
(CompilerErrorCode::XMissingDynamicDirectiveArgumentEnd, true)
}
ErrorCode::ABRUPT_CLOSING_OF_EMPTY_COMMENT => {
(CompilerErrorCode::AbruptClosingOfEmptyComment, false)
}
ErrorCode::CDATA_IN_HTML_CONTENT => (CompilerErrorCode::CdataInHtmlContent, false),
ErrorCode::END_TAG_WITH_ATTRIBUTES => (CompilerErrorCode::EndTagWithAttributes, false),
ErrorCode::INCORRECTLY_CLOSED_COMMENT => {
(CompilerErrorCode::IncorrectlyClosedComment, false)
}
ErrorCode::INCORRECTLY_OPENED_COMMENT => {
(CompilerErrorCode::IncorrectlyOpenedComment, false)
}
ErrorCode::INVALID_FIRST_CHARACTER_OF_TAG_NAME => {
(CompilerErrorCode::InvalidFirstCharacterOfTagName, false)
}
ErrorCode::MISSING_WHITESPACE_BETWEEN_ATTRIBUTES => {
(CompilerErrorCode::MissingWhitespaceBetweenAttributes, false)
}
ErrorCode::NESTED_COMMENT => (CompilerErrorCode::NestedComment, false),
ErrorCode::UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME => {
(CompilerErrorCode::UnexpectedCharacterInAttributeName, false)
}
ErrorCode::UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE => (
CompilerErrorCode::UnexpectedCharacterInUnquotedAttributeValue,
false,
),
ErrorCode::UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME => (
CompilerErrorCode::UnexpectedEqualsSignBeforeAttributeName,
false,
),
ErrorCode::UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME => (
CompilerErrorCode::UnexpectedQuestionMarkInsteadOfTagName,
false,
),
_ => return,
};
let span = Span::new(index.saturating_sub(1), index);
if severity_is_error {
self.diagnostics
.push(Diagnostic::error("syntax", compiler_code).with_span(span));
} else {
self.diagnostics
.push(Diagnostic::warning("syntax", compiler_code).with_span(span));
}
}
}
impl Syntax {
fn handle_root_open<'alloc>(
&mut self,
se: &StackElement,
name: &[u8],
_ctx: &SyntaxPluginContext<'alloc>,
) {
match resolve_root_kind(name) {
RootNodeKind::Template => {
let root = RootNodeTemplate {
tag_open: make_open_tag(se),
tag_close: None,
lang: self.prop_lang.take(),
attributes: self.take_props(),
content: Some(RootNodeTemplateContent {
start: se.tag_open_end,
end: se.tag_open_end,
children: SmallVec::new(),
}),
};
self.ast_builder = Some(TemplateAstBuilder::new(root));
self.reset_prop_state();
}
RootNodeKind::Script | RootNodeKind::Style | RootNodeKind::Unknown => {
}
}
}
fn store_script_node<'alloc>(
&mut self,
se: &StackElement,
tag_close: Option<NodeTag>,
content: Option<Span>,
ctx: &SyntaxPluginContext<'alloc>,
) {
let node = RootNodeScript {
tag_open: make_open_tag(se),
tag_close,
is_setup: self.prop_setup,
src: self.prop_src.take(),
generic: self.prop_generic.take(),
attrs: self.prop_attrs.take(),
lang: self.prop_lang.take().map(|lang| {
ScriptLanguage::from_bytes(&ctx.bytes[lang.start as usize..lang.end as usize])
}),
attributes: self.take_props(),
content,
};
if self.prop_setup {
if self.script_setup_node.is_some() {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::DuplicateScriptSetup)
.with_span(Span::new(se.tag_open_start, se.tag_open_end)),
);
}
self.script_setup_node = Some(node);
} else {
if self.script_node.is_some() {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::DuplicateScript)
.with_span(Span::new(se.tag_open_start, se.tag_open_end)),
);
}
self.script_node = Some(node);
}
self.reset_prop_state();
}
fn store_style_node<'alloc>(
&mut self,
se: &StackElement,
tag_close: Option<NodeTag>,
content: Option<Span>,
ctx: &SyntaxPluginContext<'alloc>,
) {
if self.prop_scoped {
self.has_style_scope = true;
}
if self.prop_module {
self.has_style_module = true;
}
let node = RootNodeStyle {
tag_open: make_open_tag(se),
tag_close,
lang: self.prop_lang.take().map(|lang| {
StyleLang::from_bytes(&ctx.bytes[lang.start as usize..lang.end as usize])
}),
scoped: self.prop_scoped,
module: self.prop_module,
attributes: self.take_props(),
content,
};
self.style_nodes.push(node);
self.reset_prop_state();
}
fn store_unknown_node(
&mut self,
se: &StackElement,
tag_close: Option<NodeTag>,
content: Option<Span>,
) {
let node = RootNodeUnknown {
tag_open: make_open_tag(se),
tag_close,
attributes: self.take_props(),
content,
};
self.unknown_nodes.push(node);
self.reset_prop_state();
}
fn reset_prop_state(&mut self) {
self.prop_lang = None;
self.prop_src = None;
self.prop_generic = None;
self.prop_attrs = None;
self.prop_setup = false;
self.prop_scoped = false;
self.prop_module = false;
}
}
impl Syntax {
fn handle_attribute_name(&mut self, start: u32, name_end: u32) {
self.current_prop = Some(NodeProp {
start,
name_end,
is_directive: false,
arg_start: None,
arg_end: None,
is_dynamic: None,
value_start: None,
value_end: None,
modifiers: SmallVec::new(),
});
}
fn handle_directive_name(&mut self, start: u32, name_end: u32) {
if name_end - start == 2 {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XMissingDirectiveName)
.with_span(Span::new(start, name_end)),
);
}
self.current_prop = Some(NodeProp {
start,
name_end,
is_directive: true,
arg_start: None,
arg_end: None,
is_dynamic: None,
value_start: None,
value_end: None,
modifiers: SmallVec::new(),
});
}
fn handle_directive_arg(&mut self, arg_start: u32, arg_end: u32, is_dynamic: bool) {
if let Some(prop) = &mut self.current_prop {
prop.arg_start = Some(arg_start);
prop.arg_end = Some(arg_end);
prop.is_dynamic = Some(is_dynamic);
}
}
fn handle_directive_modifier(&mut self, modifier_start: u32, modifier_end: u32) {
if let Some(prop) = &mut self.current_prop {
prop.modifiers.push(Span::new(modifier_start, modifier_end));
}
}
fn handle_attribute_value(&mut self, value_start: u32) {
if let Some(prop) = &mut self.current_prop {
prop.value_start = Some(value_start);
}
}
fn handle_attribute_end<'alloc>(
&mut self,
end: u32,
quote: QuoteType,
ctx: &SyntaxPluginContext<'alloc>,
) {
let Some(mut prop) = self.current_prop.take() else {
return;
};
if let Some(vs) = prop.value_start {
prop.value_end = Some(match quote {
QuoteType::Single | QuoteType::Double => {
if end > vs {
end - 1
} else {
vs
}
}
QuoteType::Unquoted => end,
QuoteType::NoValue => vs,
});
}
if !prop.is_directive {
let attr_name = &ctx.bytes[prop.start as usize..prop.name_end as usize];
if !self.seen_attr_names.insert(attr_name.to_vec()) {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::DuplicateAttribute)
.with_span(Span::new(prop.start, prop.name_end)),
);
}
}
let is_root_tag = !self.template_mode && self.stack_elements.len() == 1;
if is_root_tag {
let attr_name = &ctx.bytes[prop.start as usize..prop.name_end as usize];
let value_span = prop
.value_start
.zip(prop.value_end)
.map(|(vs, ve)| Span::new(vs, ve));
let root_name = self
.stack_elements
.last()
.expect("invariant: stack non-empty when is_root_tag")
.name_bytes(ctx);
let root_kind = resolve_root_kind(root_name);
match attr_name {
b"lang" => self.prop_lang = value_span,
b"setup" if root_kind == RootNodeKind::Script => self.prop_setup = true,
b"src" if root_kind == RootNodeKind::Script => self.prop_src = value_span,
b"generic" if root_kind == RootNodeKind::Script => self.prop_generic = value_span,
b"attrs" if root_kind == RootNodeKind::Script => self.prop_attrs = value_span,
b"attributes" if root_kind == RootNodeKind::Script => {
if self.prop_attrs.is_none() {
self.prop_attrs = value_span
}
}
b"scoped" if root_kind == RootNodeKind::Style => self.prop_scoped = true,
b"module" if root_kind == RootNodeKind::Style => self.prop_module = true,
b"vapor" if root_kind == RootNodeKind::Template => self.is_vapor = true,
_ => {}
}
}
if let Some(builder) = self.ast_builder.as_mut() {
let mut prop = Some(prop);
if prop.as_ref().expect("invariant: prop is Some").is_directive {
let p = prop.as_ref().expect("invariant: prop is Some");
let dir_name = &ctx.bytes[p.start as usize..p.name_end as usize];
let prop_start = p.start;
let prop_name_end = p.name_end;
macro_rules! warn_if_dup {
($is_dup:expr) => {
if $is_dup {
self.diagnostics.push(
Diagnostic::warning(
"syntax",
CompilerErrorCode::XDuplicateDirective,
)
.with_span(Span::new(prop_start, prop_name_end)),
);
}
};
}
match dir_name {
b"v-if" | b"v-else-if" | b"v-else" => {
let kind = match dir_name {
b"v-if" => ElementNodeConditionKind::If,
b"v-else-if" => ElementNodeConditionKind::ElseIf,
_ => ElementNodeConditionKind::Else,
};
if !matches!(kind, ElementNodeConditionKind::Else) {
let p = prop.as_ref().expect("invariant: prop is Some");
if !prop_has_value(p) {
self.diagnostics.push(
Diagnostic::error(
"syntax",
CompilerErrorCode::XVIfNoExpression,
)
.with_span(Span::new(prop_start, prop_name_end)),
);
}
}
let cond = ElementNodeCondition {
kind,
prop: prop
.take()
.expect("invariant: prop not yet taken in v-if branch"),
};
warn_if_dup!(builder.set_v_condition(cond));
}
b"v-for" => {
let p = prop.as_ref().expect("invariant: prop is Some");
if !prop_has_value(p) {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XVForNoExpression)
.with_span(Span::new(prop_start, prop_name_end)),
);
} else {
let val_s = p.value_start.unwrap() as usize;
let val_e = p.value_end.unwrap() as usize;
let val = &ctx.input[val_s..val_e];
if !has_v_for_separator(val) {
self.diagnostics.push(
Diagnostic::error(
"syntax",
CompilerErrorCode::XVForMalformedExpression,
)
.with_span(Span::new(val_s as u32, val_e as u32)),
);
}
}
warn_if_dup!(builder.set_v_for(
prop.take()
.expect("invariant: prop not yet taken in v-for branch"),
));
}
b"v-slot" | b"#" => {
if let Some(tag_type) = builder.current_tag_type() {
if !matches!(tag_type, TagType::Component | TagType::Template) {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XVSlotMisplaced)
.with_span(Span::new(prop_start, prop_name_end)),
);
}
}
let mut slot_prop = prop
.take()
.expect("invariant: prop not yet taken in v-slot branch");
if let Some(last_mod) = slot_prop.modifiers.last() {
slot_prop.arg_end = Some(last_mod.end);
slot_prop.modifiers.clear();
}
warn_if_dup!(builder.set_v_slot(slot_prop));
}
b"v-once" => {
warn_if_dup!(builder.set_v_once(
prop.take()
.expect("invariant: prop not yet taken in v-once branch"),
));
}
b"v-bind" | b":" => {
let p = prop
.as_ref()
.expect("invariant: prop not yet taken in v-bind branch");
if let (Some(arg_s), Some(arg_e)) = (p.arg_start, p.arg_end) {
let arg = &ctx.bytes[arg_s as usize..arg_e as usize];
match arg {
b"key" => builder.add_prop_flag(PropFlags::HasDynamicKey),
b"class" => builder.add_prop_flag(PropFlags::HasDynamicClass),
b"style" => builder.add_prop_flag(PropFlags::HasDynamicStyle),
_ => builder.add_prop_flag(PropFlags::HasDynamicBinding),
}
} else {
builder.add_prop_flag(PropFlags::HasBindSpread);
}
}
b"v-on" | b"@" => {
let p = prop
.as_ref()
.expect("invariant: prop not yet taken in v-on branch");
if p.arg_start.is_some() {
builder.add_prop_flag(PropFlags::HasEventListener);
} else {
builder.add_prop_flag(PropFlags::HasOnSpread);
}
}
b"v-model" => {
let p = prop
.as_ref()
.expect("invariant: prop is Some in v-model branch");
if !prop_has_value(p) {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XVModelNoExpression)
.with_span(Span::new(prop_start, prop_name_end)),
);
} else {
let val_s = p.value_start.unwrap() as usize;
let val_e = p.value_end.unwrap() as usize;
let val = ctx.input[val_s..val_e].trim();
if !is_member_expression(val) {
self.diagnostics.push(
Diagnostic::error(
"syntax",
CompilerErrorCode::XVModelMalformedExpression,
)
.with_span(Span::new(val_s as u32, val_e as u32)),
);
}
}
builder.add_prop_flag(PropFlags::HasModel);
}
b"v-show" => {
builder.add_prop_flag(PropFlags::HasShow);
}
b"v-html" => {
builder.add_prop_flag(PropFlags::HasVHtml);
}
b"v-text" => {
builder.add_prop_flag(PropFlags::HasVText);
}
b"v-pre" | b"v-cloak" | b"v-memo" => {}
_ => {
builder.add_prop_flag(PropFlags::HasCustomDirective);
}
}
} else {
let p = prop
.as_ref()
.expect("invariant: prop is Some in non-directive branch");
let attr_name = &ctx.bytes[p.start as usize..p.name_end as usize];
match attr_name {
b"ref" => {
builder.add_prop_flag(PropFlags::HasRef);
builder.set_v_ref(
prop.take()
.expect("invariant: prop not yet taken in ref branch"),
);
}
b"class" => builder.add_prop_flag(PropFlags::HasStaticClass),
b"style" => builder.add_prop_flag(PropFlags::HasStaticStyle),
_ => {}
}
}
if let Some(prop) = prop {
builder.push_prop_to_current(prop);
}
} else if is_root_tag {
self.element_props.push(prop);
}
}
}
impl Syntax {
fn handle_text_leaf(&mut self, start: u32, end: u32, is_entity: bool) {
if let Some(b) = self.ast_builder.as_mut() {
b.add_text(start, end, is_entity);
}
}
fn handle_comment_leaf(&mut self, start: u32, end: u32, content_start: u32, content_end: u32) {
if let Some(b) = self.ast_builder.as_mut() {
b.add_comment(start, end, content_start, content_end);
}
}
fn handle_interpolation_leaf(
&mut self,
start: u32,
end: u32,
inner_start: u32,
inner_end: u32,
) {
if let Some(b) = self.ast_builder.as_mut() {
b.add_interpolation(start, end, inner_start, inner_end);
}
}
}
impl Syntax {
fn validate_v_condition_adjacency<'alloc>(
&mut self,
id: NodeId,
ctx: &SyntaxPluginContext<'alloc>,
) {
let builder = match self.ast_builder.as_ref() {
Some(b) => b,
None => return,
};
let ast = &builder.ast;
let node = &ast.nodes[id.0];
let AstNodeKind::Element(el) = &node.kind else {
return;
};
let cond = match &el.v_condition {
Some(c) => c,
None => return,
};
if matches!(cond.kind, ElementNodeConditionKind::If) {
return;
}
let tag_span = Span::new(el.tag_open.start, el.tag_open.name_end);
let mut prev = ast.prev_sibling(id);
while let Some(prev_id) = prev {
let prev_node = &ast.nodes[prev_id.0];
match &prev_node.kind {
AstNodeKind::Comment(_) => {
prev = ast.prev_sibling(prev_id);
}
AstNodeKind::Text(t) => {
let text_bytes = &ctx.bytes[t.start as usize..t.end as usize];
if text_bytes.iter().all(|b| b.is_ascii_whitespace()) {
prev = ast.prev_sibling(prev_id);
} else {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XVElseNoAdjacentIf)
.with_span(tag_span),
);
return;
}
}
AstNodeKind::Element(prev_el) => {
if let Some(prev_cond) = &prev_el.v_condition {
if matches!(
prev_cond.kind,
ElementNodeConditionKind::If | ElementNodeConditionKind::ElseIf
) {
return;
}
}
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XVElseNoAdjacentIf)
.with_span(tag_span),
);
return;
}
AstNodeKind::Interpolation(_) => {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XVElseNoAdjacentIf)
.with_span(tag_span),
);
return;
}
}
}
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XVElseNoAdjacentIf).with_span(tag_span),
);
}
}
impl Syntax {
fn validate_v_if_same_key<'alloc>(&mut self, id: NodeId, ctx: &SyntaxPluginContext<'alloc>) {
let builder = match self.ast_builder.as_ref() {
Some(b) => b,
None => return,
};
let ast = &builder.ast;
let node = &ast.nodes[id.0];
let AstNodeKind::Element(el) = &node.kind else {
return;
};
if el.v_condition.is_none() {
return;
}
let my_key = match find_key_value(el, ctx.bytes) {
Some(k) => k,
None => return,
};
let key_span = Span::new(el.tag_open.start, el.tag_open.name_end);
let mut prev = ast.prev_sibling(id);
while let Some(prev_id) = prev {
let prev_node = &ast.nodes[prev_id.0];
match &prev_node.kind {
AstNodeKind::Element(prev_el) => {
if prev_el.v_condition.is_some() {
if let Some(prev_key) = find_key_value(prev_el, ctx.bytes) {
if my_key == prev_key {
self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XVIfSameKey)
.with_span(key_span),
);
return;
}
}
if let Some(ref cond) = prev_el.v_condition {
if matches!(cond.kind, ElementNodeConditionKind::If) {
return;
}
}
} else {
return;
}
}
AstNodeKind::Comment(_) | AstNodeKind::Text(_) => {
}
AstNodeKind::Interpolation(_) => {
return; }
}
prev = ast.prev_sibling(prev_id);
}
}
}
fn find_key_value<'a>(el: &ElementNode, bytes: &'a [u8]) -> Option<&'a [u8]> {
for prop in &el.props {
if !prop.is_directive {
continue;
}
if let (Some(arg_start), Some(arg_end)) = (prop.arg_start, prop.arg_end) {
let arg = &bytes[arg_start as usize..arg_end as usize];
if arg == b"key" {
if let (Some(vs), Some(ve)) = (prop.value_start, prop.value_end) {
if ve > vs {
return Some(&bytes[vs as usize..ve as usize]);
}
}
}
}
}
None
}
impl Syntax {
fn validate_slot_names<'alloc>(&mut self, id: NodeId, ctx: &SyntaxPluginContext<'alloc>) {
let builder = match self.ast_builder.as_ref() {
Some(b) => b,
None => return,
};
let ast = &builder.ast;
let node = &ast.nodes[id.0];
let AstNodeKind::Element(el) = &node.kind else {
return;
};
if !matches!(el.tag_type, TagType::Component) {
return;
}
let Some(content) = &el.content else {
return;
};
let mut seen_slot_names: smallvec::SmallVec<[(&[u8], Span); 4]> = smallvec::SmallVec::new();
for &child_id in &content.children {
let child = &ast.nodes[child_id.0];
let AstNodeKind::Element(child_el) = &child.kind else {
continue;
};
let Some(ref v_slot) = child_el.v_slot else {
continue;
};
let slot_name: &[u8] = match (v_slot.arg_start, v_slot.arg_end) {
(Some(s), Some(e)) if e > s => &ctx.bytes[s as usize..e as usize],
_ => b"default",
};
let slot_span = Span::new(v_slot.start, v_slot.name_end);
if let Some((_, first_span)) =
seen_slot_names.iter().find(|(name, _)| *name == slot_name)
{
let _ = first_span; self.diagnostics.push(
Diagnostic::error("syntax", CompilerErrorCode::XVSlotDuplicateSlotNames)
.with_span(slot_span),
);
} else {
seen_slot_names.push((slot_name, slot_span));
}
}
}
}
#[inline]
fn prop_has_value(prop: &NodeProp) -> bool {
match (prop.value_start, prop.value_end) {
(Some(s), Some(e)) => e > s,
_ => false,
}
}
fn has_v_for_separator(expr: &str) -> bool {
let bytes = expr.as_bytes();
let len = bytes.len();
for i in 0..len {
if i + 3 < len
&& (bytes[i] == b' ' || bytes[i] == b')' || bytes[i] == b'\n' || bytes[i] == b'\t')
{
let rest = &bytes[i + 1..];
if (rest.starts_with(b"in ") || rest.starts_with(b"in\t") || rest.starts_with(b"in\n"))
|| (rest.starts_with(b"of ")
|| rest.starts_with(b"of\t")
|| rest.starts_with(b"of\n"))
{
return true;
}
}
}
false
}
fn strip_ts_as_suffix(expr: &str) -> &str {
if let Some(pos) = expr.rfind(" as ") {
let before = expr[..pos].trim();
if !before.is_empty() {
return before;
}
}
expr
}
fn is_member_expression(expr: &str) -> bool {
let trimmed = expr.trim();
if trimmed.is_empty() {
return false;
}
let trimmed = strip_ts_as_suffix(trimmed);
let bytes = trimmed.as_bytes();
let mut bracket_depth = 0i32;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'$' => {}
b'.' => {}
b'?' if i + 1 < bytes.len() && bytes[i + 1] == b'.' => {
i += 1; }
b'[' => bracket_depth += 1,
b']' => {
bracket_depth -= 1;
if bracket_depth < 0 {
return false;
}
}
b'"' | b'\'' | b'`' => {
if bracket_depth > 0 {
let quote = bytes[i];
i += 1;
while i < bytes.len() && bytes[i] != quote {
if bytes[i] == b'\\' {
i += 1;
}
i += 1;
}
} else {
return false;
}
}
b' ' | b'\t' | b'\n' | b'\r' => {
if bracket_depth == 0 {
return false;
}
}
_ => return false,
}
i += 1;
}
bracket_depth == 0
}
impl Syntax {
#[inline]
fn take_props(&mut self) -> Vec<NodeProp> {
self.element_props.drain(..).collect()
}
}