use vize_carton::{Box, String, appends, directive::parse_vize_directive};
use vize_relief::{
ast::*,
errors::{CompilerError, ErrorCode},
};
use super::{CurrentElement, Parser, ParserStackEntry};
const MAX_ELEMENT_NESTING_DEPTH: usize = 256;
const NESTING_TOO_DEEP_MESSAGE: &str = "Element nesting is too deep.";
impl<'a> Parser<'a> {
pub(super) fn on_text_impl(&mut self, start: usize, end: usize) {
if start >= end {
return;
}
let source = self.get_source(start, end).to_owned();
self.append_or_merge_text(&source, start, end);
}
pub(super) fn on_text_entity_impl(&mut self, ch: char, start: usize, end: usize) {
let mut content = [0_u8; 4];
self.append_or_merge_text(ch.encode_utf8(&mut content), start, end);
}
fn append_or_merge_text(&mut self, content: &str, start: usize, end: usize) {
let merge_start_off = match self.stack.last().and_then(|e| e.element.children.last()) {
Some(TemplateChildNode::Text(t)) => Some(t.loc.start.offset as usize),
_ => None,
};
if let Some(merge_start) = merge_start_off {
let end_pos = self.get_pos(end);
let source_span = self.get_source(merge_start, end).into();
if let Some(entry) = self.stack.last_mut()
&& let Some(TemplateChildNode::Text(text_node)) = entry.element.children.last_mut()
{
text_node.content.push_str(content);
text_node.loc.end = end_pos;
text_node.loc.source = source_span;
}
} else {
let loc = self.create_loc(start, end);
let text_node = TextNode::new(content, loc);
let boxed = Box::new_in(text_node, self.allocator);
self.add_child(TemplateChildNode::Text(boxed));
}
}
pub(super) fn on_interpolation_impl(&mut self, start: usize, end: usize) {
let raw_content = self.get_source(start, end);
let content = raw_content.trim();
let leading_ws = raw_content.len() - raw_content.trim_start().len();
let trimmed_start = start + leading_ws;
let trimmed_end = trimmed_start + content.len();
let delim_len = self.options.delimiters.0.len();
let full_start = start - delim_len;
let full_end = end + self.options.delimiters.1.len();
let loc = self.create_loc(full_start, full_end);
let inner_loc = self.create_loc(trimmed_start, trimmed_end);
let expr = SimpleExpressionNode::new(content, false, inner_loc);
let expr_boxed = Box::new_in(expr, self.allocator);
let interp = InterpolationNode {
content: ExpressionNode::Simple(expr_boxed),
loc,
};
let boxed = Box::new_in(interp, self.allocator);
self.add_child(TemplateChildNode::Interpolation(boxed));
}
pub(super) fn on_open_tag_name_impl(&mut self, start: usize, end: usize) {
let tag = self.get_source(start, end);
let ns = if self.should_force_html_namespace(tag) {
Namespace::Html
} else {
(self.options.get_namespace)(tag, self.stack.last().map(|e| e.element.tag.as_str()))
};
self.current_element = Some(CurrentElement {
tag: tag.into(),
tag_start: start,
tag_end: end,
ns,
is_self_closing: false,
props: vize_carton::Vec::new_in(self.allocator),
});
}
pub(super) fn on_open_tag_end_impl(&mut self, end: usize) {
if let Some(current) = self.current_element.take() {
let tag_start = current.tag_start;
let loc = self.create_loc(tag_start.saturating_sub(1), end + 1);
let mut element = ElementNode::new(self.allocator, current.tag.clone(), loc);
element.ns = current.ns;
element.is_self_closing = current.is_self_closing;
element.props = current.props;
element.tag_type = self.determine_element_type(&element);
let is_pre = (self.options.is_pre_tag)(element.tag.as_str());
let has_v_pre = element
.props
.iter()
.any(|p| matches!(p, PropNode::Directive(d) if d.name == "pre"));
if has_v_pre {
let allocator = self.allocator;
let mut i = 0;
while i < element.props.len() {
if let PropNode::Directive(dir) = &element.props[i] {
if dir.name == "pre" {
element.props.remove(i);
continue;
}
let attr_name = {
let prefix = dir.raw_name.as_deref().unwrap_or(&dir.name);
let arg_str = dir.arg.as_ref().map(|a| match a {
ExpressionNode::Simple(s) => s.content.as_str(),
ExpressionNode::Compound(c) => c.loc.source.as_str(),
});
if let Some(arg) = arg_str {
let mut name =
vize_carton::String::with_capacity(prefix.len() + arg.len());
name.push_str(prefix);
name.push_str(arg);
name
} else {
vize_carton::String::from(prefix)
}
};
let attr_value = dir.exp.as_ref().map(|e| {
let content = match e {
ExpressionNode::Simple(s) => s.loc.source.clone(),
ExpressionNode::Compound(c) => c.loc.source.clone(),
};
TextNode {
content,
loc: dir.loc.clone(),
}
});
let attr = PropNode::Attribute(Box::new_in(
AttributeNode {
name: attr_name,
name_loc: dir.loc.clone(),
value: attr_value,
loc: dir.loc.clone(),
},
allocator,
));
element.props[i] = attr;
}
i += 1;
}
}
if current.is_self_closing || (self.options.is_void_tag)(element.tag.as_str()) {
let boxed = Box::new_in(element, self.allocator);
self.add_child(TemplateChildNode::Element(boxed));
} else if self.stack.len() >= MAX_ELEMENT_NESTING_DEPTH {
self.errors.push(CompilerError::with_message(
ErrorCode::ExtendPoint,
NESTING_TOO_DEEP_MESSAGE,
Some(element.loc.clone()),
));
let boxed = Box::new_in(element, self.allocator);
self.add_child(TemplateChildNode::Element(boxed));
} else {
self.stack.push(ParserStackEntry {
element,
in_pre: self.in_pre,
in_v_pre: self.in_v_pre,
});
self.in_pre = is_pre || self.in_pre;
self.in_v_pre = has_v_pre || self.in_v_pre;
}
}
}
pub(super) fn on_self_closing_tag_impl(&mut self, _end: usize) {
if let Some(ref mut current) = self.current_element {
current.is_self_closing = true;
}
}
pub(super) fn on_close_tag_impl(&mut self, start: usize, end: usize) {
let tag = self.get_source(start, end);
let mut found = false;
for i in (0..self.stack.len()).rev() {
if self.stack[i].element.tag.eq_ignore_ascii_case(tag) {
found = true;
let mut elements: vize_carton::Vec<'a, ParserStackEntry<'a>> =
vize_carton::Vec::new_in(self.allocator);
while self.stack.len() > i {
if let Some(entry) = self.stack.pop() {
elements.push(entry);
} else {
break;
}
}
for entry in elements.iter().skip(1) {
let loc = entry.element.loc.clone();
self.errors
.push(CompilerError::new(ErrorCode::MissingEndTag, Some(loc)));
}
for entry in elements.into_iter().rev() {
let in_pre = entry.in_pre;
let in_v_pre = entry.in_v_pre;
let boxed = Box::new_in(entry.element, self.allocator);
self.add_child(TemplateChildNode::Element(boxed));
self.in_pre = in_pre;
self.in_v_pre = in_v_pre;
}
break;
}
}
if !found {
let loc = self.create_loc(start.saturating_sub(2), end + 1); self.errors
.push(CompilerError::new(ErrorCode::InvalidEndTag, Some(loc)));
}
}
pub(super) fn determine_element_type(&self, element: &ElementNode<'a>) -> ElementType {
let tag = element.tag.as_str();
if tag == "slot" {
return ElementType::Slot;
}
if tag == "template" {
let has_structural_directive = element.props.iter().any(|p| {
matches!(p, PropNode::Directive(d) if matches!(d.name.as_str(), "if" | "else-if" | "else" | "for" | "slot"))
});
if has_structural_directive {
return ElementType::Template;
}
}
if self.is_component(tag) {
return ElementType::Component;
}
ElementType::Element
}
pub(super) fn is_component(&self, tag: &str) -> bool {
if matches!(
tag,
"Teleport"
| "Suspense"
| "KeepAlive"
| "BaseTransition"
| "Transition"
| "TransitionGroup"
) {
return true;
}
if let Some(is_custom) = self.options.is_custom_element
&& is_custom(tag)
{
return false;
}
if self.options.custom_renderer {
return tag.chars().next().is_some_and(|c| c.is_uppercase()) || tag.contains('-');
}
if let Some(is_native) = self.options.is_native_tag {
if !is_native(tag) {
return true;
}
} else {
if tag.chars().next().is_some_and(|c| c.is_uppercase()) {
return true;
}
}
false
}
fn should_force_html_namespace(&self, tag: &str) -> bool {
if !self.options.custom_renderer {
return false;
}
if matches!(tag, "svg" | "math") {
return false;
}
if self
.stack
.last()
.is_some_and(|entry| matches!(entry.element.ns, Namespace::Svg | Namespace::MathMl))
{
return false;
}
tag.chars().next().is_some_and(|c| c.is_lowercase())
&& !tag.contains('-')
&& !vize_carton::is_html_tag(tag)
}
pub(super) fn on_comment_impl(&mut self, start: usize, end: usize) {
let content = self.get_source(start, end);
let loc_start = start.saturating_sub(4);
let loc_end = end.saturating_add(3).min(self.source.len());
let loc = self.create_loc(loc_start, loc_end);
let directive = parse_vize_directive(content, loc.start.line, loc.start.offset);
if directive.is_none() && !self.options.comments {
return;
}
let mut comment = CommentNode::new(content, loc);
comment.directive = directive.map(|d| d.kind);
let boxed = Box::new_in(comment, self.allocator);
self.add_child(TemplateChildNode::Comment(boxed));
}
pub(super) fn on_cdata_impl(&mut self, start: usize, end: usize) {
let is_html_ns = self
.stack
.last()
.map(|e| e.element.ns)
.unwrap_or(Namespace::Html)
== Namespace::Html;
if is_html_ns {
self.on_error_impl(ErrorCode::CdataInHtmlContent, start.saturating_sub(9));
} else {
self.on_text_impl(start, end);
}
}
pub(super) fn on_error_impl(&mut self, code: ErrorCode, index: usize) {
let len = self.source.len();
let start = index.min(len);
let end = (index + 1).min(len);
let loc = self.create_loc(start, end);
let error = if let Some(message) = self.recovery_error_message(code) {
CompilerError::with_message(code, message, Some(loc))
} else {
CompilerError::new(code, Some(loc))
};
self.errors.push(error);
}
fn recovery_error_message(&self, code: ErrorCode) -> Option<String> {
match code {
ErrorCode::EofBeforeTagName => Some(
"Unexpected end of input after `<`; treating it as text so parsing can continue."
.into(),
),
ErrorCode::EofInTag => Some(
"Unexpected end of input inside a tag; inferred the missing tag close so parsing can continue."
.into(),
),
ErrorCode::EofInComment => Some(
"Comment is missing its closing `-->`; preserving the unfinished comment so parsing can finish."
.into(),
),
ErrorCode::InvalidFirstCharacterOfTagName => Some(
"Tag name starts with an invalid character; treating the malformed tag as text.".into(),
),
ErrorCode::MissingAttributeValue => {
let name = self
.current_attr
.as_ref()
.map(|attr| attr.name.as_str())
.or_else(|| self.current_dir.as_ref().map(|dir| dir.raw_name.as_str()))
.unwrap_or("attribute");
let mut message = String::with_capacity(name.len() + 70);
appends!(
message,
"Attribute `",
name,
"` is missing a value after `=`; continuing without the value."
);
Some(message)
}
ErrorCode::MissingDynamicDirectiveArgumentEnd => Some(
"Dynamic directive argument is missing its closing `]`; inferred the argument end at the next tag boundary."
.into(),
),
ErrorCode::MissingInterpolationEnd => {
let delimiter = self.options.delimiters.1.as_str();
let mut message = String::with_capacity(delimiter.len() + 97);
appends!(
message,
"Interpolation is missing its closing delimiter `",
delimiter,
"`; treating the unfinished interpolation as text."
);
Some(message)
}
ErrorCode::UnexpectedCharacterInAttributeName => Some(
"Attribute name contains an invalid character; inferred the nearest attribute boundary and continued."
.into(),
),
ErrorCode::UnexpectedCharacterInUnquotedAttributeValue => Some(
"Unquoted attribute value contains a character that should be quoted; keeping it in the value and continuing."
.into(),
),
ErrorCode::UnexpectedEqualsSignBeforeAttributeName => Some(
"Unexpected `=` before an attribute name; skipping it and continuing with the next attribute."
.into(),
),
ErrorCode::MissingWhitespaceBetweenAttributes => Some(
"Missing whitespace between attributes; inferred a new attribute boundary.".into(),
),
ErrorCode::IncorrectlyClosedComment => Some(
"Comment was closed as `--!>`; treating it as `-->` so parsing can continue."
.into(),
),
ErrorCode::IncorrectlyOpenedComment => Some(
"Declaration or comment syntax is malformed; skipping it until the next `>`.".into(),
),
_ => None,
}
}
}