use smallvec::SmallVec;
use crate::ast::types::{
AstNode, AstNodeKind, ChildrenFlag, ChildrenFlags, ChildrenMode, CommentNode, ElementContent,
ElementNode, ElementNodeCondition, InterpolationNode, PropFlag, PropFlags, TagType,
TemplateAst, TextNode,
};
use crate::parser::types::RootNodeTemplate;
use crate::types::{NodeId, NodeProp, NodeTag};
#[inline]
fn element_mut(node: &mut AstNode) -> &mut ElementNode {
match &mut node.kind {
AstNodeKind::Element(el) => el,
other => unreachable!(
"builder invariant violated: expected Element node, found {:?}",
std::mem::discriminant(other)
),
}
}
#[inline]
fn element_ref(node: &AstNode) -> &ElementNode {
match &node.kind {
AstNodeKind::Element(el) => el,
other => unreachable!(
"builder invariant violated: expected Element node, found {:?}",
std::mem::discriminant(other)
),
}
}
pub struct TemplateAstBuilder {
pub ast: TemplateAst,
open_stack: Vec<NodeId>,
}
impl TemplateAstBuilder {
pub fn new(root: RootNodeTemplate) -> Self {
Self {
ast: TemplateAst::new(root),
open_stack: Vec::with_capacity(8),
}
}
pub fn open_element(&mut self, tag_open: NodeTag) {
let id = self
.ast
.alloc_node(AstNodeKind::Element(Box::new(ElementNode {
tag_open,
tag_close: None,
tag_type: TagType::Element, is_self_closing: false, props: Vec::with_capacity(4),
content: None,
v_condition: None,
v_for: None,
v_slot: None,
v_once: None,
v_ref: None,
prop_flag: PropFlag::empty(),
children_flag: ChildrenFlag::empty(), children_mode: ChildrenMode::Empty, is_fully_static: false, })));
self.open_stack.push(id);
}
pub fn set_tag_open_end(&mut self, end: u32) {
let Some(&id) = self.open_stack.last() else {
debug_assert!(false, "set_tag_open_end called with empty open_stack");
return;
};
let el = element_mut(&mut self.ast.nodes[id.0]);
el.tag_open.end = end;
}
pub fn mark_element_content_start(&mut self, start: u32) {
let Some(&id) = self.open_stack.last() else {
return;
};
let el = element_mut(&mut self.ast.nodes[id.0]);
el.content.get_or_insert_with(|| ElementContent {
start,
end: start,
children: SmallVec::new(),
});
}
pub fn close_element(&mut self, tag_close: Option<NodeTag>, content_end: u32) -> NodeId {
let id = self
.open_stack
.pop()
.expect("close_element called with empty open_stack");
let (children_flag, children_mode) = self.compute_children_meta(id);
let is_fully_static = self.compute_is_fully_static(id, children_flag);
{
let el = element_mut(&mut self.ast.nodes[id.0]);
el.tag_close = tag_close;
el.children_flag = children_flag;
el.children_mode = children_mode;
el.is_fully_static = is_fully_static;
if let Some(content) = el.content.as_mut() {
content.end = content_end;
}
}
if let Some(&parent_id) = self.open_stack.last() {
self.ast.attach_to_parent(parent_id, id);
} else {
self.ast.attach_to_root(id);
}
id
}
fn compute_children_meta(&self, id: NodeId) -> (ChildrenFlag, ChildrenMode) {
let el = element_ref(&self.ast.nodes[id.0]);
let Some(content) = &el.content else {
let flag = ChildrenFlag::empty();
return (flag, flag.mode());
};
let children = &content.children;
if children.is_empty() {
let flag = ChildrenFlag::empty();
return (flag, flag.mode());
}
let mut flag = ChildrenFlag::empty();
let mut significant_count: u32 = 0;
for &child_id in children {
let child = &self.ast.nodes[child_id.0];
match &child.kind {
AstNodeKind::Text(_) => {
flag = flag.add(ChildrenFlags::HasText);
significant_count += 1;
}
AstNodeKind::Interpolation(_) => {
flag = flag.add(ChildrenFlags::HasInterpolation);
significant_count += 1;
}
AstNodeKind::Element(child_el) => {
flag = flag.add(ChildrenFlags::HasElement);
significant_count += 1;
if child_el.v_condition.is_some() {
flag = flag.add(ChildrenFlags::HasVIf);
}
if child_el.v_for.is_some() {
flag = flag.add(ChildrenFlags::HasVFor);
}
if child_el.v_slot.is_some() {
flag = flag.add(ChildrenFlags::HasChildWithVSlot);
if child_el
.v_slot
.as_ref()
.and_then(|p| p.is_dynamic)
.unwrap_or(false)
{
flag = flag.add(ChildrenFlags::HasDynamicSlotChild);
}
}
if child_el.prop_flag.has(PropFlags::HasDynamicKey) {
flag = flag.add(ChildrenFlags::HasChildWithKey);
}
}
AstNodeKind::Comment(_) => {
flag = flag.add(ChildrenFlags::HasComment);
}
}
}
if significant_count == 1 {
flag = flag.add(ChildrenFlags::SingleChild);
}
(flag, flag.mode())
}
fn compute_is_fully_static(&self, id: NodeId, children_flag: ChildrenFlag) -> bool {
let el = element_ref(&self.ast.nodes[id.0]);
if !el.tag_type.is_element() {
return false;
}
if el.v_condition.is_some()
|| el.v_for.is_some()
|| el.v_slot.is_some()
|| el.v_once.is_some()
|| el.v_ref.is_some()
{
return false;
}
if el.prop_flag.has_any(PropFlag::NEEDS_OXC_MASK) {
return false;
}
if children_flag.has(ChildrenFlags::HasInterpolation) {
return false;
}
if children_flag.has_structural() {
return false;
}
if children_flag.has(ChildrenFlags::HasChildWithVSlot) {
return false;
}
if let Some(content) = &el.content {
for &child_id in &content.children {
if let AstNodeKind::Element(child_el) = &self.ast.nodes[child_id.0].kind {
if !child_el.is_fully_static {
return false;
}
}
}
}
true
}
pub fn push_prop_to_current(&mut self, prop: NodeProp) {
let Some(&id) = self.open_stack.last() else {
return;
};
element_mut(&mut self.ast.nodes[id.0]).props.push(prop);
}
fn set_cached_directive<T>(
&mut self,
value: T,
field: impl FnOnce(&mut ElementNode) -> &mut Option<T>,
) -> bool {
let Some(&id) = self.open_stack.last() else {
return false;
};
let slot = field(element_mut(&mut self.ast.nodes[id.0]));
if slot.is_some() {
return true;
}
*slot = Some(value);
false
}
pub fn set_v_condition(&mut self, condition: ElementNodeCondition) -> bool {
self.set_cached_directive(condition, |el| &mut el.v_condition)
}
pub fn set_v_for(&mut self, prop: NodeProp) -> bool {
self.set_cached_directive(prop, |el| &mut el.v_for)
}
pub fn set_v_slot(&mut self, prop: NodeProp) -> bool {
self.set_cached_directive(prop, |el| &mut el.v_slot)
}
pub fn set_v_once(&mut self, prop: NodeProp) -> bool {
self.set_cached_directive(prop, |el| &mut el.v_once)
}
pub fn set_v_ref(&mut self, prop: NodeProp) -> bool {
self.set_cached_directive(prop, |el| &mut el.v_ref)
}
pub fn current_tag_type(&self) -> Option<TagType> {
let id = self.open_stack.last()?;
let node = &self.ast.nodes[id.0];
if let AstNodeKind::Element(el) = &node.kind {
Some(el.tag_type)
} else {
None
}
}
pub fn set_tag_type(&mut self, tag_type: TagType) {
let Some(&id) = self.open_stack.last() else {
debug_assert!(false, "set_tag_type called with empty open_stack");
return;
};
element_mut(&mut self.ast.nodes[id.0]).tag_type = tag_type;
}
pub fn set_self_closing(&mut self) {
let Some(&id) = self.open_stack.last() else {
debug_assert!(false, "set_self_closing called with empty open_stack");
return;
};
element_mut(&mut self.ast.nodes[id.0]).is_self_closing = true;
}
pub fn add_prop_flag(&mut self, flag: PropFlags) {
let Some(&id) = self.open_stack.last() else {
debug_assert!(false, "add_prop_flag called with empty open_stack");
return;
};
let el = element_mut(&mut self.ast.nodes[id.0]);
el.prop_flag = el.prop_flag.add(flag);
}
fn attach_leaf(&mut self, leaf_id: NodeId) {
if let Some(&parent_id) = self.open_stack.last() {
self.ast.attach_to_parent(parent_id, leaf_id);
} else {
self.ast.attach_to_root(leaf_id);
}
}
pub fn add_text(&mut self, start: u32, end: u32, is_entity: bool) -> NodeId {
let id = self.ast.alloc_node(AstNodeKind::Text(TextNode {
start,
end,
is_entity,
}));
self.attach_leaf(id);
id
}
pub fn add_comment(
&mut self,
start: u32,
end: u32,
content_start: u32,
content_end: u32,
) -> NodeId {
let id = self.ast.alloc_node(AstNodeKind::Comment(CommentNode {
start,
end,
content_start,
content_end,
}));
self.attach_leaf(id);
id
}
pub fn add_interpolation(
&mut self,
start: u32,
end: u32,
inner_start: u32,
inner_end: u32,
) -> NodeId {
let id = self
.ast
.alloc_node(AstNodeKind::Interpolation(InterpolationNode {
start,
end,
inner_start,
inner_end,
}));
self.attach_leaf(id);
id
}
#[cfg(test)]
pub fn has_open_elements(&self) -> bool {
!self.open_stack.is_empty()
}
pub fn finish(self) -> TemplateAst {
debug_assert!(
self.open_stack.is_empty(),
"TemplateAstBuilder::finish() called with {} unclosed element(s) on the open stack. \
The caller must close all elements (or force-close on EOF) before finishing.",
self.open_stack.len()
);
self.ast
}
}
#[cfg(test)]
#[path = "builder_tests.rs"]
mod builder_tests;