use vize_carton::{Box, String, Vec, capitalize, is_builtin_directive, is_native_tag};
use crate::errors::ErrorCode;
use crate::steps::expression::process_inline_handler;
use crate::steps::v_slot::validate_v_slot_usage;
use crate::{
ConstantType, DirectiveNode, ElementNode, ElementType, ExpressionNode, InterpolationNode,
PropNode, RuntimeHelper, SimpleExpressionNode, SourceLocation,
};
use super::{ExitFns, TransformContext};
fn is_dynamic_component(el: &ElementNode<'_>) -> bool {
el.tag == "component" || (el.tag == "Component" && has_is_attribute(el))
}
pub fn transform_element<'a>(
ctx: &mut TransformContext<'a>,
el: &mut Box<'a, ElementNode<'a>>,
) -> Option<ExitFns<'a>> {
maybe_promote_element_to_component(ctx, el);
validate_v_slot_usage(ctx, el);
process_element_props(ctx, el);
match el.tag_type {
ElementType::Element => {
ctx.helper(RuntimeHelper::CreateElementVNode);
}
ElementType::Component => {
if is_dynamic_component(el) {
return None;
}
let is_in_bindings = ctx
.options
.binding_metadata
.as_ref()
.map(|m| m.bindings.contains_key(el.tag.as_str()))
.unwrap_or(false);
if !is_in_bindings {
ctx.helper(RuntimeHelper::ResolveComponent);
}
let tag = el.tag.clone();
let mut exits = ExitFns::new();
exits.push(std::boxed::Box::new(move |ctx| {
ctx.add_component(tag);
}));
return Some(exits);
}
ElementType::Slot => {
ctx.helper(RuntimeHelper::RenderSlot);
}
ElementType::Template => {
ctx.helper(RuntimeHelper::Fragment);
}
}
None
}
fn maybe_promote_element_to_component(
ctx: &TransformContext<'_>,
el: &mut Box<'_, ElementNode<'_>>,
) {
if el.tag_type != ElementType::Element {
return;
}
let looks_like_component = el.tag == "component"
|| el.tag.chars().next().is_some_and(|c| c.is_uppercase())
|| el.tag.contains('-');
if looks_like_component {
el.tag_type = ElementType::Component;
return;
}
let has_is = has_is_attribute(el);
if has_is {
el.tag_type = ElementType::Component;
return;
}
if is_native_tag(&el.tag) {
return;
}
if is_registered_component(ctx, &el.tag) {
el.tag_type = ElementType::Component;
}
}
fn has_is_attribute(el: &ElementNode<'_>) -> bool {
el.props.iter().any(|prop| match prop {
PropNode::Directive(dir) => {
dir.name == "is"
|| (dir.name == "bind"
&& matches!(
&dir.arg,
Some(ExpressionNode::Simple(arg)) if arg.content == "is"
))
}
PropNode::Attribute(attr) => attr.name == "is",
})
}
fn is_registered_component(ctx: &TransformContext<'_>, tag: &str) -> bool {
if ctx.is_component_registered(tag) {
return true;
}
if tag.contains('-') || tag.contains('_') {
let camel = vize_carton::camelize(tag);
if ctx.is_component_registered(camel.as_str()) {
return true;
}
let pascal = capitalize(&camel);
return ctx.is_component_registered(pascal.as_str());
}
let pascal = capitalize(tag);
ctx.is_component_registered(pascal.as_str())
}
fn process_directive_expressions<'a>(
ctx: &mut TransformContext<'a>,
el: &mut Box<'a, ElementNode<'a>>,
) {
use crate::steps::expression::{process_expression, process_inline_handler};
for prop in el.props.iter_mut() {
if let PropNode::Directive(dir) = prop {
match dir.name.as_str() {
"bind" | "show" | "if" | "else-if" | "for" | "memo" => {
if let Some(exp) = &dir.exp {
let processed = process_expression(ctx, exp, false);
dir.exp = Some(processed);
}
}
"on" => {
if let Some(exp) = &dir.exp {
if dir.arg.is_none() {
let processed = process_expression(ctx, exp, false);
dir.exp = Some(processed);
} else {
let processed = process_inline_handler(ctx, exp);
dir.exp = Some(processed);
}
}
}
"model" => {
if let Some(exp) = &dir.exp {
let processed = process_expression(ctx, exp, false);
dir.exp = Some(processed);
}
}
_ => {
if let Some(exp) = &dir.exp {
let processed = process_expression(ctx, exp, false);
dir.exp = Some(processed);
}
if let Some(arg) = &dir.arg
&& let ExpressionNode::Simple(simple_arg) = arg
&& !simple_arg.is_static
{
let processed = process_expression(ctx, arg, false);
dir.arg = Some(processed);
}
}
}
}
}
}
fn process_element_props<'a>(ctx: &mut TransformContext<'a>, el: &mut Box<'a, ElementNode<'a>>) {
if el.props.is_empty()
|| !el
.props
.iter()
.any(|prop| matches!(prop, PropNode::Directive(_)))
{
return;
}
let allocator = ctx.allocator;
let is_component = el.tag_type == ElementType::Component;
#[cfg(feature = "legacy")]
if ctx.supports_v2_event_sugar() {
for prop in el.props.iter_mut() {
if let PropNode::Directive(dir) = prop
&& dir.name == "on"
{
crate::steps::legacy::desugar_v2_v_on_modifiers(dir);
}
}
}
if ctx.options.prefix_identifiers || ctx.options.is_ts {
process_directive_expressions(ctx, el);
}
let mut model_indices: std::vec::Vec<usize> = std::vec::Vec::new();
for (i, prop) in el.props.iter().enumerate() {
if let PropNode::Directive(dir) = prop {
match dir.name.as_str() {
"model" if !ctx.options.vapor => {
model_indices.push(i);
}
"slot" => {
ctx.helper(RuntimeHelper::RenderSlot);
}
"show" => {}
_ if !is_builtin_directive(&dir.name) => {
ctx.helper(RuntimeHelper::WithDirectives);
ctx.helper(RuntimeHelper::ResolveDirective);
ctx.add_directive(dir.name.clone());
}
_ => {}
}
}
}
struct VModelData {
idx: usize,
value_exp: String,
raw_value_exp: String,
prop_name: String,
event_name: String,
handler: String,
dir_loc: SourceLocation,
modifiers_obj: Option<String>,
modifiers_key: Option<String>,
is_dynamic: bool,
}
let mut vmodel_data: std::vec::Vec<VModelData> = std::vec::Vec::new();
let mut invalid_model_indices: std::vec::Vec<usize> = std::vec::Vec::new();
for &idx in model_indices.iter() {
if let Some(PropNode::Directive(dir)) = el.props.get(idx) {
let (value_exp, raw_value_exp) = match &dir.exp {
Some(ExpressionNode::Simple(s)) if !s.content.trim().is_empty() => {
(s.content.clone(), s.loc.source.clone())
}
Some(ExpressionNode::Compound(c)) if !c.loc.source.trim().is_empty() => {
(c.loc.source.clone(), c.loc.source.clone())
}
_ => {
ctx.on_error(ErrorCode::VModelNoExpression, Some(dir.loc.clone()));
invalid_model_indices.push(idx);
continue;
}
};
let value_exp = value_exp.trim();
if ctx.is_in_scope(value_exp) {
ctx.on_error(ErrorCode::VModelOnScope, Some(dir.loc.clone()));
invalid_model_indices.push(idx);
continue;
}
if !is_component && dir.arg.is_some() {
ctx.on_error(ErrorCode::VModelArgOnElement, Some(dir.loc.clone()));
invalid_model_indices.push(idx);
continue;
}
let value_exp = String::new(value_exp);
let is_dynamic = dir.arg.as_ref().is_some_and(|arg| match arg {
ExpressionNode::Simple(exp) => !exp.is_static,
ExpressionNode::Compound(_) => true,
});
let prop_name = dir
.arg
.as_ref()
.map(|arg| match arg {
ExpressionNode::Simple(exp) => exp.content.clone(),
ExpressionNode::Compound(exp) => exp.loc.source.clone(),
})
.unwrap_or_else(|| {
if is_component {
String::new("modelValue")
} else {
String::new("value")
}
});
let event_name = if is_component {
let mut name = String::with_capacity(9 + prop_name.len());
name.push_str("onUpdate:");
name.push_str(prop_name.as_str());
name
} else {
let has_lazy = dir.modifiers.iter().any(|m| m.content == "lazy");
if has_lazy {
String::new("onChange")
} else {
String::new("onInput")
}
};
let handler = if is_component {
let mut out = String::with_capacity(raw_value_exp.len() + 20);
out.push_str("$event => ((");
out.push_str(raw_value_exp.as_str());
out.push_str(") = $event)");
out
} else {
let has_number = dir.modifiers.iter().any(|m| m.content == "number");
let has_trim = dir.modifiers.iter().any(|m| m.content == "trim");
let mut target_value = String::from("$event.target.value");
if has_trim {
target_value.push_str(".trim()");
}
if has_number {
let mut wrapped = String::with_capacity(target_value.len() + 11);
wrapped.push_str("_toNumber(");
wrapped.push_str(&target_value);
wrapped.push(')');
target_value = wrapped;
}
let mut out = String::with_capacity(raw_value_exp.len() + target_value.len() + 16);
out.push_str("$event => ((");
out.push_str(raw_value_exp.as_str());
out.push_str(") = ");
out.push_str(target_value.as_str());
out.push(')');
out
};
let dir_loc = dir.loc.clone();
let (modifiers_obj, modifiers_key) = if is_component && !dir.modifiers.is_empty() {
let modifiers_content: std::vec::Vec<String> = dir
.modifiers
.iter()
.map(|m| {
let mut item = String::with_capacity(m.content.len() + 6);
item.push_str(&m.content);
item.push_str(": true");
item
})
.collect();
let mut obj = String::from("{ ");
for (i, item) in modifiers_content.iter().enumerate() {
if i > 0 {
obj.push_str(", ");
}
obj.push_str(item);
}
obj.push_str(" }");
let key = if prop_name == "modelValue" {
String::new("modelModifiers")
} else {
let mut key = String::with_capacity(prop_name.len() + 9);
key.push_str(prop_name.as_str());
key.push_str("Modifiers");
key
};
(Some(obj), Some(key))
} else {
(None, None)
};
vmodel_data.push(VModelData {
idx,
value_exp,
raw_value_exp,
prop_name,
event_name,
handler,
dir_loc,
modifiers_obj,
modifiers_key,
is_dynamic,
});
}
}
if !invalid_model_indices.is_empty() {
for data in vmodel_data.iter_mut() {
let removed_before = invalid_model_indices
.iter()
.filter(|&&invalid_idx| invalid_idx < data.idx)
.count();
data.idx -= removed_before;
}
for &idx in invalid_model_indices.iter().rev() {
el.props.remove(idx);
}
}
if is_component {
let static_vmodel: std::vec::Vec<_> =
vmodel_data.iter().filter(|d| !d.is_dynamic).collect();
let dynamic_vmodel: std::vec::Vec<_> =
vmodel_data.iter().filter(|d| d.is_dynamic).collect();
for data in static_vmodel.iter().rev() {
el.props.remove(data.idx);
}
for data in static_vmodel.iter() {
let value_prop = PropNode::Directive(Box::new_in(
DirectiveNode {
name: String::new("bind"),
raw_name: None,
arg: Some(ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(
data.prop_name.clone(),
true,
data.dir_loc.clone(),
),
allocator,
))),
exp: Some(ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode {
content: data.value_exp.clone(),
is_static: false,
const_type: ConstantType::NotConstant,
loc: data.dir_loc.clone(),
js_ast: None,
hoisted: None,
identifiers: None,
is_handler_key: false,
is_ref_transformed: true, },
allocator,
))),
modifiers: Vec::new_in(allocator),
for_parse_result: None,
shorthand: false,
loc: data.dir_loc.clone(),
},
allocator,
));
el.props.push(value_prop);
let raw_handler_expr = ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(data.handler.as_str(), false, data.dir_loc.clone()),
allocator,
));
let processed_handler = process_inline_handler(ctx, &raw_handler_expr);
let event_prop = PropNode::Directive(Box::new_in(
DirectiveNode {
name: String::new("on"),
raw_name: None,
arg: Some(ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(
&data.event_name[2..],
true,
data.dir_loc.clone(),
), allocator,
))),
exp: Some(processed_handler),
modifiers: Vec::new_in(allocator),
for_parse_result: None,
shorthand: false,
loc: data.dir_loc.clone(),
},
allocator,
));
el.props.push(event_prop);
if let (Some(modifiers_obj), Some(modifiers_key)) =
(&data.modifiers_obj, &data.modifiers_key)
{
let modifiers_prop = PropNode::Directive(Box::new_in(
DirectiveNode {
name: String::new("bind"),
raw_name: None,
arg: Some(ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(
modifiers_key.clone(),
true,
data.dir_loc.clone(),
),
allocator,
))),
exp: Some(ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(
modifiers_obj.as_str(),
false,
data.dir_loc.clone(),
),
allocator,
))),
modifiers: Vec::new_in(allocator),
for_parse_result: None,
shorthand: false,
loc: SourceLocation::STUB,
},
allocator,
));
el.props.push(modifiers_prop);
}
}
if !dynamic_vmodel.is_empty() {
ctx.helper(RuntimeHelper::NormalizeProps);
}
} else {
for data in vmodel_data.iter().rev() {
let mut handler = String::with_capacity(data.raw_value_exp.len() + 20);
handler.push_str("$event => ((");
handler.push_str(data.raw_value_exp.as_str());
handler.push_str(") = $event)");
let raw_handler_expr = ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new(handler.as_str(), false, data.dir_loc.clone()),
allocator,
));
let processed_handler = process_inline_handler(ctx, &raw_handler_expr);
let event_prop = PropNode::Directive(Box::new_in(
DirectiveNode {
name: String::new("on"),
raw_name: None,
arg: Some(ExpressionNode::Simple(Box::new_in(
SimpleExpressionNode::new("update:modelValue", true, data.dir_loc.clone()),
allocator,
))),
exp: Some(processed_handler),
modifiers: Vec::new_in(allocator),
for_parse_result: None,
shorthand: false,
loc: data.dir_loc.clone(),
},
allocator,
));
el.props.insert(data.idx + 1, event_prop);
}
}
}
pub fn transform_interpolation<'a>(
ctx: &mut TransformContext<'a>,
interp: &mut Box<'a, InterpolationNode<'a>>,
) {
ctx.helper(RuntimeHelper::ToDisplayString);
if ctx.options.prefix_identifiers || ctx.options.is_ts {
use crate::steps::expression::process_expression;
let processed = process_expression(ctx, &interp.content, false);
interp.content = processed;
}
}
#[cfg(test)]
#[allow(clippy::disallowed_macros)]
mod tests {
use bumpalo::Bump;
use super::transform_element;
use crate::{
PropNode, TemplateChildNode,
errors::{CompilerError, ErrorCode},
lane::{ParentNode, TransformContext, traverse::traverse_children},
options::TransformOptions,
parser::parse,
};
fn transform_errors(source: &str) -> std::vec::Vec<CompilerError> {
let allocator = Bump::new();
let (mut root, errors) = parse(&allocator, source);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
let mut ctx =
TransformContext::new(&allocator, root.source.clone(), TransformOptions::default());
traverse_children(&mut ctx, ParentNode::Root(&mut root as *mut _));
ctx.errors
}
fn assert_no_model_update_handler(props: &[PropNode<'_>]) {
assert!(!props.iter().any(|prop| matches!(
prop,
PropNode::Directive(dir)
if dir.name == "on"
&& matches!(
&dir.arg,
Some(crate::ExpressionNode::Simple(arg))
if arg.content == "update:modelValue"
)
)));
}
#[test]
fn test_transform_v_model_without_expression_reports_error() {
let allocator = Bump::new();
let (mut root, errors) = parse(&allocator, r#"<input v-model />"#);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
let mut ctx =
TransformContext::new(&allocator, root.source.clone(), TransformOptions::default());
match &mut root.children[0] {
TemplateChildNode::Element(el) => {
transform_element(&mut ctx, el);
assert!(!el.props.iter().any(|prop| matches!(
prop,
PropNode::Directive(dir) if dir.name == "model"
)));
}
other => panic!(
"Expected ElementNode, got {:?}",
std::mem::discriminant(other)
),
}
assert_eq!(ctx.errors.len(), 1);
assert_eq!(ctx.errors[0].code, ErrorCode::VModelNoExpression);
}
#[test]
fn test_transform_component_v_model_without_expression_reports_error() {
let allocator = Bump::new();
let (mut root, errors) = parse(&allocator, r#"<MyComponent v-model />"#);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
let mut ctx =
TransformContext::new(&allocator, root.source.clone(), TransformOptions::default());
match &mut root.children[0] {
TemplateChildNode::Element(el) => {
transform_element(&mut ctx, el);
assert!(!el.props.iter().any(|prop| matches!(
prop,
PropNode::Directive(dir) if dir.name == "model"
)));
}
other => panic!(
"Expected ElementNode, got {:?}",
std::mem::discriminant(other)
),
}
assert_eq!(ctx.errors.len(), 1);
assert_eq!(ctx.errors[0].code, ErrorCode::VModelNoExpression);
}
#[test]
fn test_transform_v_model_on_v_for_scope_reports_error() {
let allocator = Bump::new();
let (mut root, errors) = parse(
&allocator,
r#"<div v-for="item in items"><input v-model="item" /></div>"#,
);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
let mut ctx =
TransformContext::new(&allocator, root.source.clone(), TransformOptions::default());
traverse_children(&mut ctx, ParentNode::Root(&mut root as *mut _));
assert_eq!(ctx.errors.len(), 1);
assert_eq!(ctx.errors[0].code, ErrorCode::VModelOnScope);
match &root.children[0] {
TemplateChildNode::For(for_node) => match &for_node.children[0] {
TemplateChildNode::Element(el) => match &el.children[0] {
TemplateChildNode::Element(input) => {
assert!(!input.props.iter().any(|prop| matches!(
prop,
PropNode::Directive(dir) if dir.name == "model"
)));
assert_no_model_update_handler(input.props.as_slice());
}
other => panic!("Expected input element, got {:?}", other.node_type()),
},
other => panic!("Expected v-for child element, got {:?}", other.node_type()),
},
other => panic!("Expected ForNode, got {:?}", other.node_type()),
}
}
#[test]
fn test_transform_v_model_on_v_slot_scope_reports_error() {
let errors = transform_errors(
r#"<MyComponent v-slot="{ item }"><input v-model="item" /></MyComponent>"#,
);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, ErrorCode::VModelOnScope);
}
#[test]
fn test_transform_v_model_on_scope_property_stays_valid() {
let errors =
transform_errors(r#"<div v-for="item in items"><input v-model="item.value" /></div>"#);
assert!(errors.is_empty(), "Unexpected errors: {:?}", errors);
}
fn assert_v_model_arg_on_element_rejected(source: &str) {
let allocator = Bump::new();
let (mut root, errors) = parse(&allocator, source);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
let mut ctx =
TransformContext::new(&allocator, root.source.clone(), TransformOptions::default());
match &mut root.children[0] {
TemplateChildNode::Element(el) => {
transform_element(&mut ctx, el);
assert!(
!el.props.iter().any(|prop| matches!(
prop,
PropNode::Directive(dir) if dir.name == "model"
)),
"v-model directive should be removed for {source}"
);
assert!(
!el.props.iter().any(|prop| matches!(
prop,
PropNode::Directive(dir) if dir.name == "on"
)),
"no update handler should be emitted for {source}"
);
}
other => panic!(
"Expected ElementNode, got {:?}",
std::mem::discriminant(other)
),
}
assert_eq!(ctx.errors.len(), 1, "expected one error for {source}");
assert_eq!(ctx.errors[0].code, ErrorCode::VModelArgOnElement);
}
#[test]
fn test_transform_v_model_static_arg_on_element_reports_error() {
assert_v_model_arg_on_element_rejected(r#"<input v-model:foo="bar" />"#);
}
#[test]
fn test_transform_v_model_dynamic_arg_on_element_reports_error() {
assert_v_model_arg_on_element_rejected(r#"<input v-model:[dynKey]="value" />"#);
}
}