use vize_carton::{Box, Bump, String, Vec};
use crate::ast::*;
use crate::transform::TransformContext;
pub fn is_static_node(node: &TemplateChildNode<'_>) -> bool {
match node {
TemplateChildNode::Text(_) => true,
TemplateChildNode::Comment(_) => true,
TemplateChildNode::Element(el) => is_static_element(el),
TemplateChildNode::Interpolation(_) => false,
TemplateChildNode::If(_) => false,
TemplateChildNode::For(_) => false,
_ => false,
}
}
fn is_static_element(el: &ElementNode<'_>) -> bool {
if el.tag_type != ElementType::Element {
return false;
}
for prop in el.props.iter() {
match prop {
PropNode::Directive(_) => return false,
PropNode::Attribute(attr) => {
if attr.name == "ref" {
return false;
}
}
}
}
for child in el.children.iter() {
if matches!(child, TemplateChildNode::Comment(_)) {
return false;
}
if matches!(child, TemplateChildNode::Element(_)) {
return false;
}
if !is_static_node(child) {
return false;
}
}
true
}
pub fn get_static_type(node: &TemplateChildNode<'_>) -> StaticType {
match node {
TemplateChildNode::Text(_) => StaticType::FullyStatic,
TemplateChildNode::Comment(_) => StaticType::FullyStatic,
TemplateChildNode::Element(el) => get_element_static_type(el),
TemplateChildNode::Interpolation(_) => StaticType::NotStatic,
_ => StaticType::NotStatic,
}
}
fn get_element_static_type(el: &ElementNode<'_>) -> StaticType {
if el.tag_type != ElementType::Element {
return StaticType::NotStatic;
}
let mut has_dynamic_text = false;
for prop in el.props.iter() {
match prop {
PropNode::Directive(_) => {
return StaticType::NotStatic;
}
PropNode::Attribute(attr) => {
if attr.name == "ref" {
return StaticType::NotStatic;
}
}
}
}
for child in el.children.iter() {
match child {
TemplateChildNode::Interpolation(_) => {
has_dynamic_text = true;
}
TemplateChildNode::Element(_) => {
return StaticType::NotStatic;
}
TemplateChildNode::If(_) | TemplateChildNode::For(_) => {
return StaticType::NotStatic;
}
TemplateChildNode::Comment(_) => {
return StaticType::NotStatic;
}
_ => {}
}
}
if has_dynamic_text {
StaticType::HasDynamicText
} else {
StaticType::FullyStatic
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaticType {
NotStatic = 0,
FullyStatic = 1,
HasDynamicText = 2,
}
pub fn hoist_static<'a>(
ctx: &mut TransformContext<'a>,
children: &mut Vec<'a, TemplateChildNode<'a>>,
) {
hoist_static_inner(ctx, children, true)
}
fn hoist_static_inner<'a>(
ctx: &mut TransformContext<'a>,
children: &mut Vec<'a, TemplateChildNode<'a>>,
is_root: bool,
) {
if !ctx.options.hoist_static {
return;
}
let allocator = ctx.allocator;
let mut i = 0;
while i < children.len() {
let static_type = get_static_type(&children[i]);
match static_type {
StaticType::FullyStatic => {
if is_root {
if let TemplateChildNode::Element(el) = &mut children[i]
&& has_static_props(el)
{
hoist_element_props(ctx, el, allocator);
}
} else {
if let TemplateChildNode::Element(el) = &children[i] {
let scope_id = ctx.options.scope_id.clone();
let vnode_call =
create_vnode_call_from_element(allocator, el, scope_id.as_ref());
let hoist_index = ctx.hoist(vnode_call);
children[i] = TemplateChildNode::Hoisted(hoist_index);
ctx.helper(RuntimeHelper::CreateElementVNode);
}
}
}
StaticType::HasDynamicText => {
if let TemplateChildNode::Element(el) = &mut children[i]
&& has_static_props(el)
{
hoist_element_props(ctx, el, allocator);
}
}
StaticType::NotStatic => {
match &mut children[i] {
TemplateChildNode::Element(el) => {
hoist_static_inner(ctx, &mut el.children, false);
}
TemplateChildNode::If(if_node) => {
for branch in if_node.branches.iter_mut() {
for child in branch.children.iter_mut() {
if let TemplateChildNode::Element(el) = child {
hoist_static_inner(ctx, &mut el.children, false);
}
}
}
}
TemplateChildNode::For(for_node) => {
hoist_static_inner(ctx, &mut for_node.children, false);
}
_ => {}
}
}
}
i += 1;
}
}
fn create_vnode_call_from_element<'a>(
allocator: &'a Bump,
el: &ElementNode<'a>,
scope_id: Option<&vize_carton::String>,
) -> JsChildNode<'a> {
let tag = VNodeTag::String(el.tag.clone());
let props = create_props_expression(allocator, &el.props, scope_id);
let children = create_children_expression(allocator, &el.children);
let vnode_call = VNodeCall {
tag,
props,
children,
patch_flag: None,
dynamic_props: None,
directives: None,
is_block: false,
disable_tracking: false,
is_component: false,
loc: el.loc.clone(),
};
JsChildNode::VNodeCall(Box::new_in(vnode_call, allocator))
}
fn create_props_expression<'a>(
allocator: &'a Bump,
props: &[PropNode<'a>],
scope_id: Option<&vize_carton::String>,
) -> Option<PropsExpression<'a>> {
let mut obj_props = Vec::new_in(allocator);
for prop in props {
if let PropNode::Attribute(attr) = prop {
let key = ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(attr.name.clone(), true, attr.loc.clone()),
allocator,
));
let value_exp = if let Some(v) = &attr.value {
SimpleExpressionNode::new(v.content.clone(), true, v.loc.clone())
} else {
SimpleExpressionNode::new("true", true, attr.loc.clone())
};
let value = JsChildNode::SimpleExpression(Box::new_in(value_exp, allocator));
obj_props.push(Property {
key,
value,
loc: attr.loc.clone(),
});
}
}
if let Some(scope_id) = scope_id {
let key = ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(scope_id.clone(), true, SourceLocation::STUB),
allocator,
));
let value = JsChildNode::SimpleExpression(Box::new_in(
SimpleExpressionNode::new("", true, SourceLocation::STUB),
allocator,
));
obj_props.push(Property {
key,
value,
loc: SourceLocation::STUB,
});
}
if obj_props.is_empty() {
return None;
}
Some(PropsExpression::Object(Box::new_in(
ObjectExpression {
properties: obj_props,
loc: SourceLocation::STUB,
},
allocator,
)))
}
fn create_children_expression<'a>(
allocator: &'a Bump,
children: &Vec<'a, TemplateChildNode<'a>>,
) -> Option<VNodeChildren<'a>> {
if children.is_empty() {
return None;
}
if children.len() == 1
&& let TemplateChildNode::Text(text) = &children[0]
{
let text_node = TextNode::new(text.content.clone(), text.loc.clone());
return Some(VNodeChildren::Single(TemplateTextChildNode::Text(
Box::new_in(text_node, allocator),
)));
}
let mut all_text = true;
let mut text_content = String::default();
for child in children.iter() {
match child {
TemplateChildNode::Text(text) => {
text_content.push_str(&text.content);
}
_ => {
all_text = false;
break;
}
}
}
if all_text && !text_content.is_empty() {
let text_node = TextNode::new(text_content, SourceLocation::STUB);
return Some(VNodeChildren::Single(TemplateTextChildNode::Text(
Box::new_in(text_node, allocator),
)));
}
None
}
fn has_static_props(el: &ElementNode<'_>) -> bool {
if el.props.is_empty() {
return false;
}
for prop in el.props.iter() {
match prop {
PropNode::Directive(_) => {
return false;
}
PropNode::Attribute(attr) => {
if attr.name == "ref" {
return false;
}
}
}
}
true
}
fn hoist_element_props<'a>(
ctx: &mut TransformContext<'a>,
el: &mut ElementNode<'a>,
allocator: &'a Bump,
) {
let mut obj_props = Vec::new_in(allocator);
for prop in el.props.iter() {
if let PropNode::Attribute(attr) = prop {
let key = ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(attr.name.clone(), true, attr.loc.clone()),
allocator,
));
let value_exp = if let Some(v) = &attr.value {
SimpleExpressionNode::new(v.content.clone(), true, v.loc.clone())
} else {
SimpleExpressionNode::new("true", true, attr.loc.clone())
};
let value = JsChildNode::SimpleExpression(Box::new_in(value_exp, allocator));
obj_props.push(Property {
key,
value,
loc: attr.loc.clone(),
});
}
}
if let Some(ref scope_id) = ctx.options.scope_id {
let key = ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(scope_id.clone(), true, SourceLocation::STUB),
allocator,
));
let value = JsChildNode::SimpleExpression(Box::new_in(
SimpleExpressionNode::new("", true, SourceLocation::STUB),
allocator,
));
obj_props.push(Property {
key,
value,
loc: SourceLocation::STUB,
});
}
if obj_props.is_empty() {
return;
}
let obj_expr = ObjectExpression {
properties: obj_props,
loc: SourceLocation::STUB,
};
let js_node = JsChildNode::Object(Box::new_in(obj_expr, allocator));
let hoist_index = ctx.hoist(js_node);
el.hoisted_props_index = Some(hoist_index + 1);
}
pub fn should_use_block(el: &ElementNode<'_>) -> bool {
for prop in el.props.iter() {
if let PropNode::Directive(dir) = prop
&& (dir.name == "for" || dir.name == "if")
{
return true;
}
}
el.tag_type == ElementType::Component
}
pub fn count_dynamic_children(children: &[TemplateChildNode<'_>]) -> usize {
let mut count = 0;
for child in children {
match child {
TemplateChildNode::Interpolation(_) => count += 1,
TemplateChildNode::Element(el) => {
for prop in el.props.iter() {
if let PropNode::Directive(_) = prop {
count += 1;
break;
}
}
}
TemplateChildNode::If(_) | TemplateChildNode::For(_) => count += 1,
_ => {}
}
}
count
}
#[cfg(test)]
mod tests {
use super::{get_static_type, is_static_node};
use crate::ast::{PropNode, TemplateChildNode};
use crate::parser::parse;
use bumpalo::Bump;
#[test]
fn test_static_text() {
let allocator = Bump::new();
let (root, _) = parse(&allocator, "hello");
assert!(is_static_node(&root.children[0]));
}
#[test]
fn test_static_element() {
let allocator = Bump::new();
let (root, _) = parse(&allocator, "<div>static</div>");
assert!(is_static_node(&root.children[0]));
}
#[test]
fn test_dynamic_element() {
let allocator = Bump::new();
let (root, _) = parse(&allocator, "<div :class=\"cls\">dynamic</div>");
assert!(!is_static_node(&root.children[0]));
}
#[test]
fn test_interpolation_not_static() {
let allocator = Bump::new();
let (root, _) = parse(&allocator, "{{ msg }}");
assert!(!is_static_node(&root.children[0]));
}
#[test]
fn test_nested_dynamic_class_not_static() {
let allocator = Bump::new();
let (root, _) = parse(
&allocator,
r#"<div class="checkbox"><span class="icon" :class="{ active: checked }" /></div>"#,
);
assert!(!is_static_node(&root.children[0]));
}
#[test]
fn test_sibling_with_v_if() {
let allocator = Bump::new();
let (root, _) = parse(
&allocator,
r#"<div class="wrapper"><div class="checkbox"><span :class="{ active: checked }" /></div><label v-if="label">{{ label }}</label></div>"#,
);
if let TemplateChildNode::Element(el) = &root.children[0] {
eprintln!(
"Outer div static type: {:?}",
get_static_type(&root.children[0])
);
if let TemplateChildNode::Element(checkbox_div) = &el.children[0] {
eprintln!("checkbox div props: {:?}", checkbox_div.props.len());
eprintln!("checkbox div children: {:?}", checkbox_div.children.len());
if let TemplateChildNode::Element(span) = &checkbox_div.children[0] {
eprintln!("span props count: {:?}", span.props.len());
for prop in span.props.iter() {
match prop {
PropNode::Attribute(attr) => eprintln!(" attr: {}", attr.name),
PropNode::Directive(dir) => {
eprintln!(" directive: {} arg: {:?}", dir.name, dir.arg)
}
}
}
}
}
}
assert!(!is_static_node(&root.children[0]));
}
}