use crate::ast::*;
use super::children::generate_children;
use super::context::CodegenContext;
use super::element::is_whitespace_or_comment;
use super::expression::generate_expression;
use super::helpers::{
camelize, capitalize_first, escape_js_string, is_builtin_component, is_valid_js_identifier,
};
use super::node::generate_node;
use super::props::{generate_directive_prop_with_static, is_supported_directive};
use super::slots::{generate_slots, has_slot_children};
use vize_carton::FxHashSet;
pub fn generate_if(ctx: &mut CodegenContext, if_node: &IfNode<'_>) {
ctx.use_helper(RuntimeHelper::OpenBlock);
ctx.use_helper(RuntimeHelper::CreateComment);
for (i, branch) in if_node.branches.iter().enumerate() {
if let Some(condition) = &branch.condition {
if i == 0 {
ctx.push("(");
generate_expression(ctx, condition);
ctx.push(")");
ctx.indent();
ctx.newline();
ctx.push("? ");
} else {
ctx.newline();
ctx.push(": (");
generate_expression(ctx, condition);
ctx.push(")");
ctx.indent();
ctx.newline();
ctx.push("? ");
}
} else {
ctx.newline();
ctx.push(": ");
}
generate_if_branch(ctx, branch, i);
if branch.condition.is_some() && i > 0 {
ctx.deindent();
}
}
if if_node.branches.iter().all(|b| b.condition.is_some()) {
ctx.newline();
ctx.push(": ");
ctx.push(ctx.helper(RuntimeHelper::CreateComment));
ctx.push("(\"v-if\", true)");
}
ctx.deindent();
}
pub fn generate_if_branch(
ctx: &mut CodegenContext,
branch: &IfBranchNode<'_>,
branch_index: usize,
) {
if branch.children.len() == 1 {
match &branch.children[0] {
TemplateChildNode::Element(el) => {
if el.tag_type == ElementType::Template {
if el.children.len() == 1 {
if let TemplateChildNode::Element(inner) = &el.children[0] {
if inner.tag_type == ElementType::Component {
generate_if_branch_component(ctx, inner, branch, branch_index);
} else {
generate_if_branch_element(ctx, inner, branch, branch_index);
}
return;
}
}
generate_if_branch_template_fragment(ctx, &el.children, branch, branch_index);
} else if el.tag_type == ElementType::Component {
generate_if_branch_component(ctx, el, branch, branch_index);
} else {
generate_if_branch_element(ctx, el, branch, branch_index);
}
}
_ => {
generate_if_branch_fragment(ctx, branch, branch_index);
}
}
} else {
generate_if_branch_fragment(ctx, branch, branch_index);
}
}
pub fn generate_if_branch_component(
ctx: &mut CodegenContext,
el: &ElementNode<'_>,
branch: &IfBranchNode<'_>,
branch_index: usize,
) {
let prev_skip_scope_id = ctx.skip_scope_id;
ctx.skip_scope_id = true;
ctx.use_helper(RuntimeHelper::CreateBlock);
ctx.push("(");
ctx.push(ctx.helper(RuntimeHelper::OpenBlock));
ctx.push("(), ");
ctx.push(ctx.helper(RuntimeHelper::CreateBlock));
ctx.push("(");
if el.tag == "component" {
let dynamic_is = el.props.iter().find_map(|p| {
if let PropNode::Directive(dir) = p {
if dir.name == "bind" {
if let Some(ExpressionNode::Simple(arg)) = &dir.arg {
if arg.content == "is" {
return dir.exp.as_ref();
}
}
}
}
None
});
let static_is = el.props.iter().find_map(|p| {
if let PropNode::Attribute(attr) = p {
if attr.name == "is" {
return attr.value.as_ref().map(|v| v.content.as_str());
}
}
None
});
if let Some(is_exp) = dynamic_is {
ctx.use_helper(RuntimeHelper::ResolveDynamicComponent);
ctx.push(ctx.helper(RuntimeHelper::ResolveDynamicComponent));
ctx.push("(");
generate_expression(ctx, is_exp);
ctx.push(")");
} else if let Some(name) = static_is {
ctx.use_helper(RuntimeHelper::ResolveDynamicComponent);
ctx.push(ctx.helper(RuntimeHelper::ResolveDynamicComponent));
ctx.push("(\"");
ctx.push(name);
ctx.push("\")");
} else {
ctx.push("_component_component");
}
} else if let Some(builtin) = is_builtin_component(&el.tag) {
ctx.use_helper(builtin);
ctx.push(ctx.helper(builtin));
} else if ctx.is_component_in_bindings(&el.tag) {
if !ctx.options.inline {
ctx.push("$setup.");
}
ctx.push(el.tag.as_str());
} else {
ctx.push("_component_");
ctx.push(&el.tag.replace('-', "_"));
}
let (static_class, static_style) = extract_static_class_style(el);
let has_dyn_class = has_dynamic_class(el);
let has_dyn_style = has_dynamic_style(el);
let has_vbind_spread = has_vbind_spread(el);
if has_vbind_spread {
ctx.use_helper(RuntimeHelper::MergeProps);
ctx.push(", ");
ctx.push(ctx.helper(RuntimeHelper::MergeProps));
ctx.push("(");
let mut first_merge_arg = true;
for prop in el.props.iter() {
if let PropNode::Directive(dir) = prop {
if dir.name == "bind" && dir.arg.is_none() {
if let Some(exp) = &dir.exp {
if !first_merge_arg {
ctx.push(", ");
}
generate_expression(ctx, exp);
first_merge_arg = false;
}
}
}
}
if !first_merge_arg {
ctx.push(", ");
}
generate_if_branch_props_object(
ctx,
el,
branch,
branch_index,
static_class,
static_style,
has_dyn_class,
has_dyn_style,
);
ctx.push(")");
} else {
ctx.push(", ");
generate_if_branch_props_object(
ctx,
el,
branch,
branch_index,
static_class,
static_style,
has_dyn_class,
has_dyn_style,
);
}
ctx.skip_scope_id = prev_skip_scope_id;
if has_slot_children(el) {
ctx.push(", ");
generate_slots(ctx, el);
} else if el.children.iter().any(|c| !is_whitespace_or_comment(c)) {
ctx.push(", [");
for (i, child) in el.children.iter().enumerate() {
if i > 0 {
ctx.push(",");
}
generate_node(ctx, child);
}
ctx.push("]");
}
ctx.push("))")
}
fn should_skip_prop_for_if(
p: &PropNode<'_>,
has_dynamic_class: bool,
has_dynamic_style: bool,
) -> bool {
match p {
PropNode::Attribute(attr) => {
if attr.name == "class" && has_dynamic_class {
return true;
}
if attr.name == "style" && has_dynamic_style {
return true;
}
false
}
PropNode::Directive(dir) => {
if dir.name == "bind" {
if let Some(ExpressionNode::Simple(arg)) = &dir.arg {
if arg.content == "key" {
return true;
}
}
}
if matches!(dir.name.as_str(), "if" | "else-if" | "else") {
return true;
}
false
}
}
}
fn extract_static_class_style<'a>(el: &'a ElementNode<'_>) -> (Option<&'a str>, Option<&'a str>) {
let mut static_class = None;
let mut static_style = None;
for prop in el.props.iter() {
if let PropNode::Attribute(attr) = prop {
if attr.name == "class" {
if let Some(val) = &attr.value {
static_class = Some(val.content.as_str());
}
} else if attr.name == "style" {
if let Some(val) = &attr.value {
static_style = Some(val.content.as_str());
}
}
}
}
(static_class, static_style)
}
fn has_dynamic_class(el: &ElementNode<'_>) -> bool {
el.props.iter().any(|p| {
if let PropNode::Directive(dir) = p {
if dir.name == "bind" {
if let Some(ExpressionNode::Simple(arg)) = &dir.arg {
return arg.content == "class";
}
}
}
false
})
}
fn has_dynamic_style(el: &ElementNode<'_>) -> bool {
el.props.iter().any(|p| {
if let PropNode::Directive(dir) = p {
if dir.name == "bind" {
if let Some(ExpressionNode::Simple(arg)) = &dir.arg {
return arg.content == "style";
}
}
}
false
})
}
fn generate_single_prop_for_if(
ctx: &mut CodegenContext,
prop: &PropNode<'_>,
static_class: Option<&str>,
static_style: Option<&str>,
) {
match prop {
PropNode::Attribute(attr) => {
let needs_quotes = !is_valid_js_identifier(&attr.name);
if needs_quotes {
ctx.push("\"");
}
ctx.push(&attr.name);
if needs_quotes {
ctx.push("\"");
}
ctx.push(": ");
if let Some(value) = &attr.value {
ctx.push("\"");
ctx.push(&escape_js_string(value.content.as_str()));
ctx.push("\"");
} else {
ctx.push("\"\"");
}
}
PropNode::Directive(dir) => {
generate_directive_prop_with_static(ctx, dir, static_class, static_style);
}
}
}
pub fn generate_if_branch_element(
ctx: &mut CodegenContext,
el: &ElementNode<'_>,
branch: &IfBranchNode<'_>,
branch_index: usize,
) {
ctx.use_helper(RuntimeHelper::CreateElementBlock);
ctx.push("(");
ctx.push(ctx.helper(RuntimeHelper::OpenBlock));
ctx.push("(), ");
ctx.push(ctx.helper(RuntimeHelper::CreateElementBlock));
ctx.push("(\"");
ctx.push(el.tag.as_str());
ctx.push("\"");
let (static_class, static_style) = extract_static_class_style(el);
let has_dyn_class = has_dynamic_class(el);
let has_dyn_style = has_dynamic_style(el);
let has_vbind_spread = has_vbind_spread(el);
if has_vbind_spread {
ctx.use_helper(RuntimeHelper::MergeProps);
ctx.push(", ");
ctx.push(ctx.helper(RuntimeHelper::MergeProps));
ctx.push("(");
let mut first_merge_arg = true;
for prop in el.props.iter() {
if let PropNode::Directive(dir) = prop {
if dir.name == "bind" && dir.arg.is_none() {
if let Some(exp) = &dir.exp {
if !first_merge_arg {
ctx.push(", ");
}
generate_expression(ctx, exp);
first_merge_arg = false;
}
}
}
}
if !first_merge_arg {
ctx.push(", ");
}
generate_if_branch_props_object(
ctx,
el,
branch,
branch_index,
static_class,
static_style,
has_dyn_class,
has_dyn_style,
);
ctx.push(")");
} else {
ctx.push(", ");
generate_if_branch_props_object(
ctx,
el,
branch,
branch_index,
static_class,
static_style,
has_dyn_class,
has_dyn_style,
);
}
if !el.children.is_empty() {
ctx.push(", ");
if el.children.len() == 1 {
if let TemplateChildNode::Text(text) = &el.children[0] {
ctx.push("\"");
ctx.push(&escape_js_string(text.content.as_str()));
ctx.push("\"");
} else {
generate_if_branch_children(ctx, &el.children);
}
} else {
generate_if_branch_children(ctx, &el.children);
}
}
ctx.push("))");
}
#[allow(clippy::too_many_arguments)]
fn generate_if_branch_props_object(
ctx: &mut CodegenContext,
el: &ElementNode<'_>,
branch: &IfBranchNode<'_>,
branch_index: usize,
static_class: Option<&str>,
static_style: Option<&str>,
has_dynamic_class: bool,
has_dynamic_style: bool,
) {
let has_other_props = el.props.iter().any(|p| {
if let PropNode::Directive(dir) = p {
if !is_supported_directive(dir) {
return false;
}
}
!should_skip_prop_for_if(p, has_dynamic_class, has_dynamic_style)
&& !is_vbind_spread_prop(p)
});
let scope_id = if ctx.skip_scope_id {
None
} else {
ctx.options.scope_id.clone()
};
let has_scope = scope_id.is_some();
if !has_other_props && !has_scope {
ctx.push("{ key: ");
generate_if_branch_key(ctx, branch, branch_index);
ctx.push(" }");
return;
}
ctx.push("{");
ctx.indent();
ctx.newline();
ctx.push("key: ");
generate_if_branch_key(ctx, branch, branch_index);
let mut seen_events: FxHashSet<String> = FxHashSet::default();
for prop in el.props.iter() {
if let PropNode::Directive(dir) = prop {
if !is_supported_directive(dir) {
continue;
}
}
if should_skip_prop_for_if(prop, has_dynamic_class, has_dynamic_style) {
continue;
}
if is_vbind_spread_prop(prop) {
continue;
}
if let PropNode::Directive(dir) = prop {
if dir.name == "on" {
if let Some(key) = get_static_event_key(dir) {
if !seen_events.insert(key) {
continue;
}
}
}
}
ctx.push(",");
ctx.newline();
generate_single_prop_for_if(ctx, prop, static_class, static_style);
}
if let Some(ref scope_id) = scope_id {
ctx.push(",");
ctx.newline();
ctx.push("\"");
ctx.push(scope_id);
ctx.push("\": \"\"");
}
ctx.deindent();
ctx.newline();
ctx.push("}");
}
fn has_vbind_spread(el: &ElementNode<'_>) -> bool {
el.props.iter().any(|p| is_vbind_spread_prop(p))
}
fn is_vbind_spread_prop(prop: &PropNode<'_>) -> bool {
if let PropNode::Directive(dir) = prop {
return dir.name == "bind" && dir.arg.is_none();
}
false
}
fn get_static_event_key(dir: &DirectiveNode<'_>) -> Option<String> {
let arg = dir.arg.as_ref()?;
let ExpressionNode::Simple(exp) = arg else {
return None;
};
if !exp.is_static {
return None;
}
let mut event_name = exp.content.as_str();
let is_keyboard_event = matches!(event_name, "keydown" | "keyup" | "keypress");
let mut event_option_modifiers: Vec<&str> = Vec::new();
let mut system_modifiers: Vec<&str> = Vec::new();
for modifier in dir.modifiers.iter() {
let mod_name = modifier.content.as_str();
match mod_name {
"capture" | "once" | "passive" => {
event_option_modifiers.push(mod_name);
}
"left" | "right" => {
if !is_keyboard_event {
system_modifiers.push(mod_name);
}
}
"middle" => {
system_modifiers.push(mod_name);
}
_ => {}
}
}
let has_right_modifier = system_modifiers.contains(&"right");
let has_middle_modifier = system_modifiers.contains(&"middle");
if event_name == "click" && has_right_modifier {
event_name = "contextmenu";
} else if event_name == "click" && has_middle_modifier {
event_name = "mouseup";
}
let mut key = if event_name.contains(':') {
let parts: Vec<&str> = event_name.splitn(2, ':').collect();
if parts.len() == 2 {
let first_part = camelize(parts[0]);
let mut name = String::from("on");
if let Some(first) = first_part.chars().next() {
name.push_str(&first.to_uppercase().to_string());
name.push_str(&first_part[first.len_utf8()..]);
}
name.push(':');
name.push_str(parts[1]);
name
} else {
String::from(event_name)
}
} else {
let camelized = camelize(event_name);
let mut name = String::from("on");
if let Some(first) = camelized.chars().next() {
name.push_str(&first.to_uppercase().to_string());
name.push_str(&camelized[first.len_utf8()..]);
}
name
};
for opt_mod in &event_option_modifiers {
key.push_str(&capitalize_first(opt_mod));
}
Some(key)
}
pub fn generate_if_branch_template_fragment(
ctx: &mut CodegenContext,
children: &[TemplateChildNode<'_>],
branch: &IfBranchNode<'_>,
branch_index: usize,
) {
ctx.use_helper(RuntimeHelper::CreateElementBlock);
ctx.use_helper(RuntimeHelper::Fragment);
ctx.use_helper(RuntimeHelper::CreateElementVNode);
ctx.push("(");
ctx.push(ctx.helper(RuntimeHelper::OpenBlock));
ctx.push("(), ");
ctx.push(ctx.helper(RuntimeHelper::CreateElementBlock));
ctx.push("(");
ctx.push(ctx.helper(RuntimeHelper::Fragment));
ctx.push(", { key: ");
generate_if_branch_key(ctx, branch, branch_index);
ctx.push(" }, [");
ctx.indent();
for (i, child) in children.iter().enumerate() {
if i > 0 {
ctx.push(",");
}
ctx.newline();
generate_node(ctx, child);
}
ctx.deindent();
ctx.newline();
ctx.push("], 64 /* STABLE_FRAGMENT */))");
}
pub fn generate_if_branch_key(
ctx: &mut CodegenContext,
branch: &IfBranchNode<'_>,
branch_index: usize,
) {
if let Some(ref user_key) = branch.user_key {
match user_key {
PropNode::Attribute(attr) => {
if let Some(ref value) = attr.value {
ctx.push("\"");
ctx.push(&escape_js_string(value.content.as_str()));
ctx.push("\"");
} else {
ctx.push(&branch_index.to_string());
}
}
PropNode::Directive(dir) => {
if let Some(ref exp) = dir.exp {
generate_expression(ctx, exp);
} else {
ctx.push(&branch_index.to_string());
}
}
}
} else {
ctx.push(&branch_index.to_string());
}
}
pub fn generate_if_branch_fragment(
ctx: &mut CodegenContext,
branch: &IfBranchNode<'_>,
branch_index: usize,
) {
ctx.use_helper(RuntimeHelper::CreateElementBlock);
ctx.use_helper(RuntimeHelper::Fragment);
ctx.push("(");
ctx.push(ctx.helper(RuntimeHelper::OpenBlock));
ctx.push("(), ");
ctx.push(ctx.helper(RuntimeHelper::CreateElementBlock));
ctx.push("(");
ctx.push(ctx.helper(RuntimeHelper::Fragment));
ctx.push(", { key: ");
generate_if_branch_key(ctx, branch, branch_index);
ctx.push(" }, ");
generate_children(ctx, &branch.children);
ctx.push(", 64 /* STABLE_FRAGMENT */))");
}
pub fn generate_if_branch_children(ctx: &mut CodegenContext, children: &[TemplateChildNode<'_>]) {
if children.is_empty() {
return;
}
let has_only_text_or_interpolation = children.iter().all(|c| {
matches!(
c,
TemplateChildNode::Text(_) | TemplateChildNode::Interpolation(_)
)
});
if has_only_text_or_interpolation {
let has_interpolation = children
.iter()
.any(|c| matches!(c, TemplateChildNode::Interpolation(_)));
for (i, child) in children.iter().enumerate() {
if i > 0 {
ctx.push(" + ");
}
match child {
TemplateChildNode::Interpolation(interp) => {
ctx.use_helper(RuntimeHelper::ToDisplayString);
ctx.push(ctx.helper(RuntimeHelper::ToDisplayString));
ctx.push("(");
generate_expression(ctx, &interp.content);
ctx.push(")");
}
TemplateChildNode::Text(text) => {
ctx.push("\"");
ctx.push(&escape_js_string(text.content.as_str()));
ctx.push("\"");
}
_ => {}
}
}
if has_interpolation {
ctx.push(", 1 /* TEXT */");
}
} else {
ctx.push("[");
ctx.indent();
for (i, child) in children.iter().enumerate() {
if i > 0 {
ctx.push(",");
}
ctx.newline();
generate_node(ctx, child);
}
ctx.deindent();
ctx.newline();
ctx.push("]");
}
}