use crate::ast::{DirectiveNode, ExpressionNode, RuntimeHelper};
use super::super::{
context::CodegenContext,
expression::{generate_event_handler, generate_expression, generate_simple_expression},
helpers::{camelize, escape_js_string, is_constant_simple_expression, is_valid_js_identifier},
};
use vize_carton::String;
use vize_carton::ToCompactString;
fn is_static_expression(exp: &ExpressionNode<'_>, ctx: &CodegenContext) -> bool {
match exp {
ExpressionNode::Simple(simple) => {
is_constant_simple_expression(simple, ctx.options.binding_metadata.as_ref())
}
ExpressionNode::Compound(_) => false,
}
}
pub fn is_supported_directive(dir: &DirectiveNode<'_>) -> bool {
if dir.name == "model" {
return dir.arg.as_ref().is_some_and(|arg| match arg {
ExpressionNode::Simple(exp) => !exp.is_static,
ExpressionNode::Compound(_) => true,
});
}
matches!(dir.name.as_str(), "bind" | "on" | "html" | "text")
}
#[derive(Clone, Copy, Default)]
pub struct StaticMerge<'a> {
pub class: Option<&'a str>,
pub class_before: bool,
pub style: Option<&'a str>,
pub style_before: bool,
}
impl<'a> StaticMerge<'a> {
pub fn from_props(props: &'a [crate::ast::PropNode<'a>]) -> Self {
let mut merge = StaticMerge::default();
let mut class_index = None;
let mut style_index = None;
for (index, prop) in props.iter().enumerate() {
match prop {
crate::ast::PropNode::Attribute(attr) => {
if attr.name == "class" && merge.class.is_none() {
merge.class = attr.value.as_ref().map(|v| v.content.as_str());
class_index = Some(index);
} else if attr.name == "style" && merge.style.is_none() {
merge.style = attr.value.as_ref().map(|v| v.content.as_str());
style_index = Some(index);
}
}
crate::ast::PropNode::Directive(dir) => {
if dir.name == "bind"
&& let Some(ExpressionNode::Simple(exp)) = &dir.arg
&& exp.is_static
{
if exp.content == "class" && class_index.is_some_and(|i| i < index) {
merge.class_before = true;
} else if exp.content == "style" && style_index.is_some_and(|i| i < index) {
merge.style_before = true;
}
}
}
}
}
merge
}
}
pub fn generate_directive_prop_with_static(
ctx: &mut CodegenContext,
dir: &DirectiveNode<'_>,
static_merge: StaticMerge<'_>,
) {
generate_directive_prop_with_static_key_casing(
ctx,
dir,
static_merge,
StaticBindKeyCasing::Preserve,
);
}
pub fn generate_slot_outlet_directive_prop_with_static(
ctx: &mut CodegenContext,
dir: &DirectiveNode<'_>,
static_merge: StaticMerge<'_>,
) {
generate_directive_prop_with_static_key_casing(
ctx,
dir,
static_merge,
StaticBindKeyCasing::Camelize,
);
}
#[derive(Clone, Copy)]
enum StaticBindKeyCasing {
Preserve,
Camelize,
}
fn generate_directive_prop_with_static_key_casing(
ctx: &mut CodegenContext,
dir: &DirectiveNode<'_>,
static_merge: StaticMerge<'_>,
static_key_casing: StaticBindKeyCasing,
) {
match dir.name.as_str() {
"bind" => {
generate_vbind_prop(ctx, dir, static_merge, static_key_casing);
}
"on" => {
generate_von_prop(ctx, dir);
}
"model" => {
generate_vmodel_prop(ctx, dir);
}
"html" => {
ctx.push("innerHTML: ");
if let Some(exp) = &dir.exp {
generate_expression(ctx, exp);
} else {
ctx.push("undefined");
}
}
"text" => {
ctx.use_helper(RuntimeHelper::ToDisplayString);
ctx.push("textContent: ");
ctx.push(ctx.helper(RuntimeHelper::ToDisplayString));
ctx.push("(");
if let Some(exp) = &dir.exp {
generate_expression(ctx, exp);
} else {
ctx.push("undefined");
}
ctx.push(")");
}
_ => {
}
}
}
fn generate_vbind_prop(
ctx: &mut CodegenContext,
dir: &DirectiveNode<'_>,
static_merge: StaticMerge<'_>,
static_key_casing: StaticBindKeyCasing,
) {
let static_class = static_merge.class;
let static_style = static_merge.style;
let mut is_class = false;
let mut is_style = false;
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 let Some(ExpressionNode::Simple(exp)) = &dir.arg {
if !exp.is_static {
let emit_key_expr = |ctx: &mut CodegenContext| {
let content = exp.content.as_str();
if content.contains('.')
|| content.starts_with('_')
|| content.starts_with('$')
|| content.contains('`')
|| content.contains('(')
{
if content.starts_with('`') {
ctx.push("(");
let prefixed =
super::super::expression::generate_simple_expression_with_prefix(
ctx, content,
);
ctx.push(&prefixed);
ctx.push(")");
} else {
generate_simple_expression(ctx, exp);
}
} else {
if ctx.is_slot_param(content) {
ctx.push(content);
} else {
ctx.push("_ctx.");
ctx.push(content);
}
}
};
ctx.push("[");
if has_camel {
ctx.use_helper(RuntimeHelper::Camelize);
ctx.push("_camelize(");
emit_key_expr(ctx);
ctx.push(" || \"\")");
} else if has_prop {
ctx.push("`.${");
emit_key_expr(ctx);
ctx.push(" || \"\"}`");
} else if has_attr {
ctx.push("`^${");
emit_key_expr(ctx);
ctx.push(" || \"\"}`");
} else {
emit_key_expr(ctx);
ctx.push(" || \"\"");
}
ctx.push("]: ");
} else {
let key = &exp.content;
is_class = key == "class";
is_style = key == "style";
let base_key: vize_carton::String =
if has_camel || matches!(static_key_casing, StaticBindKeyCasing::Camelize) {
camelize(key)
} else {
key.to_compact_string()
};
let transformed_key: vize_carton::String = if has_prop {
let mut name = String::with_capacity(1 + base_key.len());
name.push('.');
name.push_str(&base_key);
name
} else if has_attr {
let mut name = String::with_capacity(1 + base_key.len());
name.push('^');
name.push_str(&base_key);
name
} else {
base_key
};
let needs_quotes = !is_valid_js_identifier(&transformed_key);
if needs_quotes {
ctx.push("\"");
}
ctx.push(&transformed_key);
if needs_quotes {
ctx.push("\"");
}
ctx.push(": ");
}
}
if let Some(exp) = &dir.exp {
let is_static_literal = is_static_expression(exp, ctx);
if is_class {
if !ctx.skip_normalize {
ctx.use_helper(RuntimeHelper::NormalizeClass);
ctx.push("_normalizeClass(");
}
if let Some(static_val) = static_class {
ctx.push("[");
if static_merge.class_before {
ctx.push("\"");
ctx.push(&escape_js_string(static_val));
ctx.push("\", ");
generate_expression(ctx, exp);
} else {
generate_expression(ctx, exp);
ctx.push(", \"");
ctx.push(&escape_js_string(static_val));
ctx.push("\"");
}
ctx.push("]");
} else {
generate_expression(ctx, exp);
}
if !ctx.skip_normalize {
ctx.push(")");
}
} else if is_style {
let needs_normalize = !ctx.skip_normalize && !is_static_literal;
if needs_normalize {
ctx.use_helper(RuntimeHelper::NormalizeStyle);
ctx.push("_normalizeStyle(");
}
if let Some(static_val) = static_style {
let emit_static_style = |ctx: &mut CodegenContext| {
ctx.push("{");
for (i, part) in static_val
.split(';')
.filter(|s| !s.trim().is_empty())
.enumerate()
{
if i > 0 {
ctx.push(",");
}
let parts: Vec<&str> = part.splitn(2, ':').collect();
if parts.len() == 2 {
let key = parts[0].trim();
let value = parts[1].trim();
ctx.push("\"");
ctx.push(key);
ctx.push("\":\"");
ctx.push(value);
ctx.push("\"");
}
}
ctx.push("}");
};
ctx.push("[");
if static_merge.style_before {
emit_static_style(ctx);
ctx.push(", ");
generate_expression(ctx, exp);
} else {
generate_expression(ctx, exp);
ctx.push(", ");
emit_static_style(ctx);
}
ctx.push("]");
} else {
generate_expression(ctx, exp);
}
if needs_normalize {
ctx.push(")");
}
} else {
generate_expression(ctx, exp);
}
} else {
ctx.push("undefined");
}
}
fn generate_von_prop(ctx: &mut CodegenContext, dir: &DirectiveNode<'_>) {
let (event_name, is_dynamic_event) = if let Some(ExpressionNode::Simple(exp)) = &dir.arg {
(exp.content.as_str(), !exp.is_static)
} else {
("", false)
};
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();
let mut key_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);
}
"native" => {}
"left" | "right" => {
if is_keyboard_event {
key_modifiers.push(mod_name);
} else {
system_modifiers.push(mod_name);
}
}
"stop" | "prevent" | "self" | "ctrl" | "shift" | "alt" | "meta" | "middle"
| "exact" => {
system_modifiers.push(mod_name);
}
"enter" | "tab" | "delete" | "esc" | "space" | "up" | "down" => {
key_modifiers.push(mod_name);
}
_ => {
key_modifiers.push(mod_name);
}
}
}
if let Some(ExpressionNode::Simple(exp)) = &dir.arg {
if is_dynamic_event {
ctx.use_helper(RuntimeHelper::ToHandlerKey);
ctx.push("[");
ctx.push(ctx.helper(RuntimeHelper::ToHandlerKey));
ctx.push("(");
let content = exp.content.as_str();
if content.contains('.') || content.starts_with('_') || content.starts_with('$') {
generate_simple_expression(ctx, exp);
} else {
ctx.push("_ctx.");
ctx.push(content);
}
ctx.push(")]: ");
} else {
let on_plain_element = ctx.props_is_plain_element && dir.raw_name.is_some();
let event_name = super::events::von_event_key_for(
exp.content.as_str(),
on_plain_element,
dir.modifiers.iter().map(|m| m.content.as_str()),
);
let needs_quotes = !is_valid_js_identifier(&event_name);
if needs_quotes {
ctx.push("\"");
}
ctx.push(&event_name);
if needs_quotes {
ctx.push("\"");
}
ctx.push(": ");
}
}
let has_system_mods = !system_modifiers.is_empty();
let has_key_mods = !key_modifiers.is_empty();
let is_const_handler = dir.exp.as_ref().is_some_and(|exp| {
if let ExpressionNode::Simple(simple) = exp
&& !simple.is_static
{
let content = simple.content.trim();
if crate::transforms::is_simple_identifier(content)
&& let Some(ref metadata) = ctx.options.binding_metadata
{
return matches!(
metadata.bindings.get(content),
Some(crate::options::BindingType::SetupConst)
);
}
}
false
});
let needs_cache =
ctx.cache_handlers_in_current_scope() && dir.exp.is_some() && !is_const_handler;
if needs_cache {
let cache_index = ctx.next_cache_index();
ctx.push("_cache[");
ctx.push(&cache_index.to_compact_string());
ctx.push("] || (_cache[");
ctx.push(&cache_index.to_compact_string());
ctx.push("] = ");
}
if has_key_mods {
ctx.use_helper(RuntimeHelper::WithKeys);
ctx.push("_withKeys(");
}
if has_system_mods {
ctx.use_helper(RuntimeHelper::WithModifiers);
ctx.push("_withModifiers(");
}
if let Some(exp) = &dir.exp {
generate_event_handler(ctx, exp, needs_cache);
} else {
ctx.push("() => {}");
}
if has_system_mods {
ctx.push(", [");
for (i, mod_name) in system_modifiers.iter().enumerate() {
if i > 0 {
ctx.push(",");
}
ctx.push("\"");
ctx.push(mod_name);
ctx.push("\"");
}
ctx.push("])");
}
if has_key_mods {
ctx.push(", [");
for (i, mod_name) in key_modifiers.iter().enumerate() {
if i > 0 {
ctx.push(",");
}
ctx.push("\"");
ctx.push(mod_name);
ctx.push("\"");
}
ctx.push("])");
}
if needs_cache {
ctx.push(")");
}
}
fn generate_vmodel_prop(ctx: &mut CodegenContext, dir: &DirectiveNode<'_>) {
if let Some(ExpressionNode::Simple(arg_exp)) = &dir.arg
&& !arg_exp.is_static
{
let prop_name = &arg_exp.content;
let value_exp = dir
.exp
.as_ref()
.map(|e| match e {
ExpressionNode::Simple(s) => s.content.as_str(),
ExpressionNode::Compound(c) => c.loc.source.as_str(),
})
.unwrap_or("undefined");
ctx.push("[_ctx.");
ctx.push(prop_name);
ctx.push("]: ");
ctx.push(value_exp);
ctx.push(",");
ctx.newline();
ctx.push("[\"onUpdate:\" + _ctx.");
ctx.push(prop_name);
ctx.push("]: $event => ((");
ctx.push(value_exp);
ctx.push(") = $event)");
if !dir.modifiers.is_empty() {
ctx.push(",");
ctx.newline();
ctx.push("[_ctx.");
ctx.push(prop_name);
ctx.push(" + \"Modifiers\"]: { ");
for (i, modifier) in dir.modifiers.iter().enumerate() {
if i > 0 {
ctx.push(", ");
}
ctx.push(&modifier.content);
ctx.push(": true");
}
ctx.push(" }");
}
}
}