use vize_carton::{Box, Bump, String, ToCompactString, Vec, camelize};
use crate::codegen::is_constant_simple_expression;
use crate::lane::TransformContext;
use crate::{
AttributeNode, DirectiveNode, ElementNode, ElementType, ExpressionNode, JsChildNode, Namespace,
ObjectExpression, PropNode, Property, PropsExpression, RuntimeHelper, SimpleExpressionNode,
SourceLocation, TemplateChildNode, TemplateTextChildNode, TextNode, VNodeCall, VNodeChildren,
VNodeTag,
};
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(_) if el.tag == "svg" => return false,
PropNode::Directive(_) if !is_hoistable_static_prop(prop) => return false,
PropNode::Directive(_) => {}
PropNode::Attribute(attr) => {
if attr.name == "ref" {
return false;
}
}
}
}
for child in el.children.iter() {
if matches!(child, TemplateChildNode::Comment(_)) {
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(_) if el.tag == "svg" => {
return StaticType::NotStatic;
}
PropNode::Directive(_) if !is_hoistable_static_prop(prop) => {
return StaticType::NotStatic;
}
PropNode::Directive(_) => {}
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(child_el) => match get_element_static_type(child_el) {
StaticType::FullyStatic => {}
_ => 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, false)
}
fn hoist_static_inner<'a>(
ctx: &mut TransformContext<'a>,
children: &mut Vec<'a, TemplateChildNode<'a>>,
is_root: bool,
hoist_static_vnodes: 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
&& let TemplateChildNode::Element(el) = &mut children[i]
&& has_static_props(el)
{
hoist_element_props(ctx, el, allocator);
} else if hoist_static_vnodes
&& let TemplateChildNode::Element(el) = &mut children[i]
{
let scope_id = ctx
.hoisted_scope_id
.clone()
.or_else(|| 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 is_root
&& 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) => {
if has_static_props(el)
&& ((is_root
&& ctx.options.inline
&& has_only_native_element_descendants(el))
|| el.ns != Namespace::Html
|| has_only_static_nested_children(el))
{
hoist_element_props(ctx, el, allocator);
}
let child_hoist_static_vnodes = hoist_static_vnodes
|| has_directives(el)
|| el.tag_type != ElementType::Element;
hoist_static_inner(ctx, &mut el.children, false, child_hoist_static_vnodes);
}
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, true);
}
}
}
}
TemplateChildNode::For(for_node) => {
hoist_static_inner(ctx, &mut for_node.children, false, true);
}
_ => {}
}
}
}
i += 1;
}
}
fn create_vnode_call_from_element<'a>(
allocator: &'a Bump,
el: &mut 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, &mut el.children, scope_id);
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);
let mut seen: vize_carton::FxHashSet<vize_carton::String> = vize_carton::FxHashSet::default();
for prop in props {
match prop {
PropNode::Attribute(attr) => {
if seen.contains(attr.name.as_str()) {
continue;
}
seen.insert(attr.name.clone());
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, attr.loc.clone())
};
let value = JsChildNode::SimpleExpression(Box::new_in(value_exp, allocator));
obj_props.push(Property {
key,
value,
loc: attr.loc.clone(),
});
}
PropNode::Directive(dir) => {
let Some((name, exp)) = hoistable_static_bind_parts(dir) else {
continue;
};
if seen.contains(name.as_str()) {
continue;
}
seen.insert(name.clone());
let key = ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(name, true, dir.loc.clone()),
allocator,
));
let value_exp = SimpleExpressionNode {
content: exp.content.clone(),
is_static: false,
const_type: exp.const_type,
loc: exp.loc.clone(),
js_ast: None,
hoisted: None,
identifiers: None,
is_handler_key: false,
is_ref_transformed: false,
};
let value = JsChildNode::SimpleExpression(Box::new_in(value_exp, allocator));
obj_props.push(Property {
key,
value,
loc: dir.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: &mut Vec<'a, TemplateChildNode<'a>>,
scope_id: Option<&vize_carton::String>,
) -> 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),
)));
}
if children
.iter()
.all(|c| matches!(c, TemplateChildNode::Text(_)))
{
let mut text_content = String::default();
for child in children.iter() {
if let TemplateChildNode::Text(text) = child {
text_content.push_str(&text.content);
}
}
if !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),
)));
}
}
let mut moved = std::mem::replace(children, Vec::new_in(allocator));
if let Some(scope_id) = scope_id {
for child in moved.iter_mut() {
inject_scope_id(allocator, child, scope_id);
}
}
Some(VNodeChildren::Multiple(moved))
}
fn inject_scope_id<'a>(
allocator: &'a Bump,
node: &mut TemplateChildNode<'a>,
scope_id: &vize_carton::String,
) {
if let TemplateChildNode::Element(el) = node {
let already = el.props.iter().any(|p| match p {
PropNode::Attribute(a) => a.name == *scope_id,
PropNode::Directive(_) => false,
});
if !already {
el.props.push(PropNode::Attribute(Box::new_in(
AttributeNode {
name: scope_id.clone(),
name_loc: SourceLocation::STUB,
value: None,
loc: SourceLocation::STUB,
},
allocator,
)));
}
for child in el.children.iter_mut() {
inject_scope_id(allocator, child, scope_id);
}
}
}
fn has_static_props(el: &ElementNode<'_>) -> bool {
if el.props.is_empty() {
return false;
}
el.props.iter().all(is_hoistable_static_prop)
}
fn is_hoistable_static_prop(prop: &PropNode<'_>) -> bool {
match prop {
PropNode::Attribute(attr) => attr.name != "ref",
PropNode::Directive(dir) => hoistable_static_bind_parts(dir).is_some(),
}
}
fn hoistable_static_bind_parts<'a>(
dir: &'a DirectiveNode<'a>,
) -> Option<(String, &'a SimpleExpressionNode<'a>)> {
if dir.name != "bind" {
return None;
}
let Some(ExpressionNode::Simple(arg)) = &dir.arg else {
return None;
};
if !arg.is_static {
return None;
}
let has_camel = dir.modifiers.iter().any(|m| m.content == "camel");
let has_prop = dir.modifiers.iter().any(|m| m.content == "prop");
let has_attr = dir.modifiers.iter().any(|m| m.content == "attr");
if dir
.modifiers
.iter()
.any(|m| !matches!(m.content.as_str(), "camel" | "prop" | "attr"))
{
return None;
}
let key = if has_camel {
camelize(&arg.content)
} else if has_prop {
let mut name = String::with_capacity(1 + arg.content.len());
name.push('.');
name.push_str(&arg.content);
name
} else if has_attr {
let mut name = String::with_capacity(1 + arg.content.len());
name.push('^');
name.push_str(&arg.content);
name
} else {
arg.content.to_compact_string()
};
if matches!(key.as_str(), "ref" | "class") {
return None;
}
let Some(ExpressionNode::Simple(exp)) = &dir.exp else {
return None;
};
if !is_constant_simple_expression(exp, None) {
return None;
}
Some((key, exp))
}
fn has_directives(el: &ElementNode<'_>) -> bool {
el.props
.iter()
.any(|prop| matches!(prop, PropNode::Directive(_)))
}
fn has_only_static_nested_children(el: &ElementNode<'_>) -> bool {
if el.children.is_empty() {
return false;
}
el.children.iter().all(is_static_nested_child)
}
fn is_static_nested_child(child: &TemplateChildNode<'_>) -> bool {
match child {
TemplateChildNode::Text(_) | TemplateChildNode::Interpolation(_) => true,
TemplateChildNode::Element(el) => is_plain_static_nested_element(el),
_ => false,
}
}
fn has_only_native_element_descendants(el: &ElementNode<'_>) -> bool {
el.children.iter().all(|child| match child {
TemplateChildNode::Text(_)
| TemplateChildNode::Interpolation(_)
| TemplateChildNode::Comment(_) => true,
TemplateChildNode::Element(child_el) if child_el.tag_type == ElementType::Element => {
has_only_native_element_descendants(child_el)
}
_ => false,
})
}
fn is_plain_static_nested_element(el: &ElementNode<'_>) -> bool {
match el.tag_type {
ElementType::Element => {
props_are_static_attrs(el) && el.children.iter().all(is_static_nested_child)
}
ElementType::Slot => props_are_static_attrs(el),
_ => false,
}
}
fn props_are_static_attrs(el: &ElementNode<'_>) -> bool {
el.props.iter().all(is_hoistable_static_prop)
}
fn hoist_element_props<'a>(
ctx: &mut TransformContext<'a>,
el: &mut ElementNode<'a>,
allocator: &'a Bump,
) {
let mut obj_props = Vec::new_in(allocator);
let mut seen: vize_carton::FxHashSet<vize_carton::String> = vize_carton::FxHashSet::default();
for prop in el.props.iter() {
match prop {
PropNode::Attribute(attr) => {
if seen.contains(attr.name.as_str()) {
continue;
}
seen.insert(attr.name.clone());
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, attr.loc.clone())
};
let value = JsChildNode::SimpleExpression(Box::new_in(value_exp, allocator));
obj_props.push(Property {
key,
value,
loc: attr.loc.clone(),
});
}
PropNode::Directive(dir) => {
let Some((name, exp)) = hoistable_static_bind_parts(dir) else {
continue;
};
if seen.contains(name.as_str()) {
continue;
}
seen.insert(name.clone());
let key = ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(name, true, dir.loc.clone()),
allocator,
));
let value_exp = SimpleExpressionNode {
content: exp.content.clone(),
is_static: false,
const_type: exp.const_type,
loc: exp.loc.clone(),
js_ast: None,
hoisted: None,
identifiers: None,
is_handler_key: false,
is_ref_transformed: false,
};
let value = JsChildNode::SimpleExpression(Box::new_in(value_exp, allocator));
obj_props.push(Property {
key,
value,
loc: dir.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::parser::parse;
use crate::{PropNode, TemplateChildNode};
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]));
}
#[test]
fn test_nested_static_element_is_static() {
let allocator = Bump::new();
let (root, _) = parse(
&allocator,
r#"<div class="outer"><span class="a">x</span></div>"#,
);
assert!(is_static_node(&root.children[0]));
assert_eq!(
get_static_type(&root.children[0]),
super::StaticType::FullyStatic
);
}
#[test]
fn test_deeply_nested_static_element_is_static() {
let allocator = Bump::new();
let (root, _) = parse(
&allocator,
r#"<div class="outer"><div class="inner"><span>deep</span></div></div>"#,
);
assert!(is_static_node(&root.children[0]));
}
#[test]
fn test_nested_with_dynamic_text_not_fully_static() {
let allocator = Bump::new();
let (root, _) = parse(
&allocator,
r#"<div class="outer"><span>{{ msg }}</span></div>"#,
);
assert_eq!(
get_static_type(&root.children[0]),
super::StaticType::NotStatic
);
}
fn compile_hoisted(src: &str) -> (String, String) {
let allocator = Bump::new();
let (mut root, _errors) = parse(&allocator, src);
let mut opts = crate::options::TransformOptions::default();
opts.hoist_static = true;
crate::lane::transform(&allocator, &mut root, opts, None);
let r = crate::codegen::generate(&root, crate::options::CodegenOptions::default());
(r.preamble.to_string(), r.code.to_string())
}
#[test]
fn test_codegen_nested_static_subtree_caches_recursively() {
let (_pre, code) = compile_hoisted(
r#"<div class="outer"><div class="inner"><span>deep</span></div></div>"#,
);
assert!(
code.contains(
"_createElementVNode(\"div\", { class: \"inner\" }, [\n _createElementVNode(\"span\", null, \"deep\")\n ], -1 /* CACHED */)"
),
"unexpected codegen:\n{code}"
);
}
#[test]
fn test_codegen_hoisted_nested_vnode_keeps_descendant() {
let (preamble, _code) =
compile_hoisted(r#"<div><p v-if="ok"><span class="a"><b>x</b></span></p></div>"#);
assert!(
preamble.contains(
"_createElementVNode(\"span\", { class: \"a\" }, [_createElementVNode(\"b\", null, \"x\")])"
),
"nested <b> was dropped from hoisted subtree:\n{preamble}"
);
}
}