use oxc_allocator::{TakeIn, Vec as ArenaVec};
use oxc_ast::{
Comment, CommentKind, NONE,
ast::{Expression, JSXAttributeItem, JSXChild, JSXExpression, PropertyKind, Statement},
};
use oxc_span::{SPAN, Span};
use vue_compiler_core::parser::{
AstNode, Directive, DirectiveArg, ElemProp, Element, SourceNode, TextNode,
};
use crate::{
is_void_tag,
parser::{
ParserImpl,
elements::{v_for::VForWrapper, v_slot::VSlotWrapper},
parse::SourceLocatonSpan,
},
};
mod directive;
mod v_for;
mod v_slot;
impl<'a> ParserImpl<'a> {
fn parse_children(
&mut self,
start: u32,
end: u32,
children: Vec<AstNode<'a>>,
) -> ArenaVec<'a, JSXChild<'a>> {
let ast = self.ast;
if children.is_empty() {
return ast.vec();
}
let mut result = self.ast.vec_with_capacity(children.len() + 2);
if let Some(first) = children.first()
&& matches!(first, AstNode::Element(_) | AstNode::Interpolation(_))
&& start != first.get_location().start.offset as u32
{
let span = Span::new(start, first.get_location().start.offset as u32);
let value = span.source_text(self.source_text);
result.push(ast.jsx_child_text(span, value, Some(ast.atom(value))));
}
let last = if let Some(last) = children.last()
&& matches!(last, AstNode::Element(_) | AstNode::Interpolation(_))
&& end != last.get_location().end.offset as u32
{
let span = Span::new(last.get_location().end.offset as u32, end);
let value = span.source_text(self.source_text);
Some(ast.jsx_child_text(span, value, Some(ast.atom(value))))
} else {
None
};
for child in children {
result.push(match child {
AstNode::Element(node) => self.parse_element(node, None),
AstNode::Text(text) => self.parse_text(&text),
AstNode::Comment(comment) => self.parse_comment(&comment),
AstNode::Interpolation(interp) => self.parse_interpolation(&interp),
});
}
if let Some(last) = last {
result.push(last);
}
result
}
pub fn parse_element(
&mut self,
node: Element<'a>,
children: Option<ArenaVec<'a, JSXChild<'a>>>,
) -> JSXChild<'a> {
let ast = self.ast;
let open_element_span = {
let start = node.location.start.offset;
let tag_name_end = if let Some(prop) = node.properties.last() {
match prop {
ElemProp::Attr(prop) => prop.location.end.offset,
ElemProp::Dir(prop) => prop.location.end.offset,
}
} else {
start + 1 + node.tag_name.len()
};
let end = memchr::memchr(b'>', &self.source_text.as_bytes()[tag_name_end..])
.map(|i| tag_name_end + i + 1)
.unwrap(); Span::new(start as u32, end as u32)
};
let location_span = node.location.span();
let tag_name = node.tag_name;
let end_element_span = {
if location_span.source_text(self.source_text).ends_with("/>") || is_void_tag!(tag_name) {
node.location.span()
} else {
let end = node.location.end.offset;
let start = self.roffset(end).saturating_sub(tag_name.len() + 3) as u32;
Span::new(start, end as u32)
}
};
let mut v_for_wrapper = VForWrapper::new(&ast);
let mut v_slot_wrapper = VSlotWrapper::new(&ast);
let mut attributes = ast.vec();
for prop in node.properties {
attributes.push(self.parse_prop(prop, &mut v_for_wrapper, &mut v_slot_wrapper));
}
let children = match children {
Some(children) => children,
None => v_slot_wrapper.wrap(self.parse_children(
open_element_span.end,
end_element_span.start,
node.children,
)),
};
v_for_wrapper.wrap(ast.jsx_element(
location_span,
ast.jsx_opening_element(
open_element_span,
ast.jsx_element_name_identifier(
Span::new(
open_element_span.start + 1,
open_element_span.start + 1 + node.tag_name.len() as u32,
),
ast.atom(node.tag_name),
),
NONE,
attributes,
),
children,
if end_element_span.eq(&location_span) {
None
} else {
Some(ast.jsx_closing_element(
end_element_span,
ast.jsx_element_name_identifier(
Span::new(
end_element_span.start + 2,
end_element_span.start + 2 + node.tag_name.len() as u32,
),
ast.atom(node.tag_name),
),
))
},
))
}
fn parse_prop(
&mut self,
prop: ElemProp<'a>,
v_for_wrapper: &mut VForWrapper<'_, 'a>,
v_slot_wrapper: &mut VSlotWrapper<'_, 'a>,
) -> JSXAttributeItem<'a> {
let ast = self.ast;
match prop {
ElemProp::Attr(attr) => {
let attr_end = self.roffset(attr.location.end.offset) as u32;
let attr_span = Span::new(attr.location.start.offset as u32, attr_end);
ast.jsx_attribute_item_attribute(
attr_span,
ast.jsx_attribute_name_identifier(attr.name_loc.span(), ast.atom(attr.name)),
if let Some(value) = attr.value {
Some(ast.jsx_attribute_value_string_literal(
Span::new(value.location.span().start + 1, attr_end - 1),
ast.atom(value.content.raw),
None,
))
} else {
None
},
)
}
ElemProp::Dir(dir) => {
let dir_start = dir.location.start.offset as u32;
let dir_end = self.roffset(dir.location.end.offset) as u32;
let dir_name = self.parse_directive_name(&dir);
if dir.name == "slot" {
self.analyze_v_slot(&dir, v_slot_wrapper, &dir_name);
} else if dir.name == "for" {
self.analyze_v_for(&dir, v_for_wrapper);
}
let value = if let Some(expr) = &dir.expression {
let expr_start = expr.location.start.offset + 1;
Some(
ast.jsx_attribute_value_expression_container(
Span::new(expr_start as u32, dir_end - 1),
((|| {
if matches!(dir.name, "for" | "slot") {
None
} else {
let expr = self.parse_expression(expr.content.raw, expr_start)?;
Some(JSXExpression::from(self.parse_dynamic_argument(&dir, expr)?))
}
})())
.unwrap_or_else(|| JSXExpression::EmptyExpression(ast.jsx_empty_expression(SPAN))),
),
)
} else if let Some(argument) = &dir.argument
&& let DirectiveArg::Dynamic(_) = argument
&& let Some(argument) =
self.parse_dynamic_argument(&dir, ast.expression_identifier(SPAN, "undefined"))
{
Some(ast.jsx_attribute_value_expression_container(SPAN, argument.into()))
} else {
None
};
ast.jsx_attribute_item_attribute(
Span::new(dir_start, dir_end),
dir_name,
value,
)
}
}
}
fn parse_dynamic_argument(
&mut self,
dir: &Directive<'a>,
expression: Expression<'a>,
) -> Option<Expression<'a>> {
let head_name = dir.head_loc.span().source_text(self.source_text);
let dir_start = dir.location.start.offset;
if let Some(argument) = &dir.argument
&& let DirectiveArg::Dynamic(argument_str) = argument
{
let dynamic_arg_expression = self.parse_expression(
argument_str,
if head_name.starts_with("v-") {
dir_start + 2 + dir.name.len() + 2 } else {
dir_start + 2 },
)?;
Some(self.ast.expression_object(
SPAN,
self.ast.vec1(self.ast.object_property_kind_object_property(
SPAN,
PropertyKind::Init,
dynamic_arg_expression.into(),
expression,
false,
false,
true,
)),
))
} else {
Some(expression)
}
}
fn parse_text(&self, text: &TextNode<'a>) -> JSXChild<'a> {
let raw = self.ast.atom(&text.text.iter().map(|t| t.raw).collect::<String>());
self.ast.jsx_child_text(text.location.span(), raw, Some(raw))
}
fn parse_comment(&mut self, comment: &SourceNode<'a>) -> JSXChild<'a> {
let ast = self.ast;
let span = comment.location.span();
self.comments.push(Comment::new(
span.start + 1,
span.end - 1,
if comment.source.contains('\n') {
CommentKind::MultiLineBlock
} else {
CommentKind::SingleLineBlock
},
));
ast.jsx_child_expression_container(span, ast.jsx_expression_empty_expression(SPAN))
}
fn parse_interpolation(&mut self, introp: &SourceNode<'a>) -> JSXChild<'a> {
let ast = self.ast;
let container_span = introp.location.span();
let expr_start = introp.location.start.offset + 2;
ast.jsx_child_expression_container(
container_span,
self
.parse_expression(introp.source, expr_start)
.map_or_else(|| ast.jsx_expression_empty_expression(SPAN), JSXExpression::from),
)
}
pub fn parse_expression(&mut self, source: &'a str, start: usize) -> Option<Expression<'a>> {
let (mut body, _) =
self.oxc_parse(&format!("({source})"), self.source_type, start.saturating_sub(1))?;
let Some(Statement::ExpressionStatement(stmt)) = body.get_mut(0) else {
unreachable!()
};
let Expression::ParenthesizedExpression(expression) = &mut stmt.expression else {
unreachable!()
};
Some(expression.expression.take_in(self.allocator))
}
fn roffset(&self, end: usize) -> usize {
end - self.source_text[..end].chars().rev().take_while(|c| c.is_whitespace()).count()
}
}