use crate::ast::*;
use crate::transforms::v_slot::{collect_slots, get_slot_name, has_v_slot};
use super::context::CodegenContext;
use super::expression::generate_expression;
use super::helpers::{escape_js_string, is_valid_js_identifier};
use super::node::generate_node;
use vize_carton::String;
use vize_carton::ToCompactString;
fn get_slot_props(dir: &DirectiveNode<'_>) -> Option<vize_carton::String> {
dir.exp.as_ref().map(|exp| match exp {
ExpressionNode::Simple(s) => s.loc.source.clone(),
ExpressionNode::Compound(c) => c.loc.source.clone(),
})
}
fn prefix_slot_defaults(source: &str) -> String {
let bytes = source.as_bytes();
let len = bytes.len();
let mut result = String::with_capacity(len + 20);
let mut i = 0;
while i < len {
if bytes[i] == b'=' {
if i + 1 < len && (bytes[i + 1] == b'=' || bytes[i + 1] == b'>') {
result.push(bytes[i] as char);
result.push(bytes[i + 1] as char);
i += 2;
continue;
}
result.push('=');
i += 1;
while i < len && (bytes[i] == b' ' || bytes[i] == b'\t') {
result.push(bytes[i] as char);
i += 1;
}
if i < len && (bytes[i].is_ascii_alphabetic() || bytes[i] == b'_' || bytes[i] == b'$') {
let start = i;
while i < len
&& (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_' || bytes[i] == b'$')
{
i += 1;
}
let ident = &source[start..i];
if !matches!(
ident,
"true" | "false" | "null" | "undefined" | "NaN" | "Infinity"
) {
result.push_str("_ctx.");
}
result.push_str(ident);
}
} else {
result.push(bytes[i] as char);
i += 1;
}
}
result
}
fn extract_slot_params(props_str: &str) -> Vec<String> {
let mut params = Vec::new();
super::v_for::extract_destructure_params(props_str.trim(), &mut params);
params
}
pub fn has_slot_children(el: &ElementNode<'_>) -> bool {
if el.children.is_empty() {
return false;
}
if matches!(
el.tag.as_str(),
"Teleport" | "teleport" | "KeepAlive" | "keep-alive"
) {
return false;
}
for prop in &el.props {
if let PropNode::Directive(dir) = prop {
if dir.name.as_str() == "slot" {
return true;
}
}
}
let has_meaningful_child = el.children.iter().any(|child| match child {
TemplateChildNode::Text(t) => !t.content.trim().is_empty(),
TemplateChildNode::Comment(_) => false,
_ => true,
});
if !has_meaningful_child {
return false;
}
true
}
pub fn has_dynamic_slots_flag(el: &ElementNode<'_>) -> bool {
let collected_slots = collect_slots(el);
if collected_slots.iter().any(|s| s.is_dynamic) {
return true;
}
if has_forwarded_slot_outlet(el) {
return true;
}
if has_dynamic_default_slot_children(el) {
return true;
}
has_conditional_or_loop_slots(el)
}
fn has_dynamic_default_slot_children(el: &ElementNode<'_>) -> bool {
el.children
.iter()
.any(|child| matches!(child, TemplateChildNode::If(_) | TemplateChildNode::For(_)))
}
fn has_forwarded_slot_outlet(el: &ElementNode<'_>) -> bool {
el.children.iter().any(child_contains_slot_outlet)
}
fn child_contains_slot_outlet(child: &TemplateChildNode<'_>) -> bool {
match child {
TemplateChildNode::Element(el) => {
if el.tag_type == ElementType::Slot || el.tag.as_str() == "slot" {
return true;
}
el.children.iter().any(child_contains_slot_outlet)
}
TemplateChildNode::If(if_node) => if_node
.branches
.iter()
.flat_map(|branch| branch.children.iter())
.any(child_contains_slot_outlet),
TemplateChildNode::For(for_node) => {
for_node.children.iter().any(child_contains_slot_outlet)
}
_ => false,
}
}
fn has_conditional_or_loop_slots(el: &ElementNode<'_>) -> bool {
el.children.iter().any(|child| match child {
TemplateChildNode::If(if_node) => if_node.branches.iter().any(|branch| {
branch.children.iter().any(|c| {
if let TemplateChildNode::Element(el) = c {
el.tag.as_str() == "template" && has_v_slot(el)
} else {
false
}
})
}),
TemplateChildNode::For(for_node) => for_node.children.iter().any(|c| {
if let TemplateChildNode::Element(el) = c {
el.tag.as_str() == "template" && has_v_slot(el)
} else {
false
}
}),
_ => false,
})
}
pub fn generate_slots(ctx: &mut CodegenContext, el: &ElementNode<'_>) {
let root_slot = el.props.iter().find_map(|p| {
if let PropNode::Directive(dir) = p {
if dir.name.as_str() == "slot" {
return Some(dir.as_ref());
}
}
None
});
let collected_slots = collect_slots(el);
let has_forwarded_slots = has_forwarded_slot_outlet(el);
let has_dynamic_slots = ctx.in_v_for
|| collected_slots.iter().any(|s| s.is_dynamic)
|| has_forwarded_slots
|| has_dynamic_default_slot_children(el);
let has_conditional_slots = has_conditional_or_loop_slots(el);
if has_conditional_slots && root_slot.is_none() {
generate_create_slots(ctx, el);
return;
}
ctx.push("{");
ctx.indent();
if let Some(slot_dir) = root_slot {
ctx.newline();
ctx.push("default: ");
ctx.use_helper(RuntimeHelper::WithCtx);
ctx.push(ctx.helper(RuntimeHelper::WithCtx));
ctx.push("(");
let params = if let Some(props_str) = get_slot_props(slot_dir) {
let processed = prefix_slot_defaults(&props_str);
ctx.push("(");
ctx.push(&processed);
ctx.push(")");
extract_slot_params(&props_str)
} else {
ctx.push("()");
vec![]
};
ctx.add_slot_params(¶ms);
ctx.push(" => [");
ctx.indent();
generate_slot_children(ctx, &el.children);
ctx.deindent();
ctx.newline();
ctx.push("])");
ctx.remove_slot_params(¶ms);
} else {
let mut has_generated_default = false;
let mut first_slot = true;
for child in &el.children {
if let TemplateChildNode::Element(template_el) = child {
if template_el.tag.as_str() == "template" && has_v_slot(template_el) {
if let Some(slot_dir) = template_el.props.iter().find_map(|p| {
if let PropNode::Directive(dir) = p {
if dir.name.as_str() == "slot" {
return Some(dir.as_ref());
}
}
None
}) {
if !first_slot {
ctx.push(",");
}
first_slot = false;
ctx.newline();
let slot_name = get_slot_name(slot_dir);
let is_dynamic = slot_dir
.arg
.as_ref()
.map(|arg| match arg {
ExpressionNode::Simple(exp) => !exp.is_static,
ExpressionNode::Compound(_) => true,
})
.unwrap_or(false);
if is_dynamic {
let trimmed_name = slot_name.trim();
if trimmed_name.starts_with('`') && trimmed_name.ends_with('`') {
let inner = &trimmed_name[1..trimmed_name.len() - 1];
ctx.push("[\"");
ctx.push(&escape_js_string(inner));
ctx.push("\"]");
} else {
ctx.push("[");
ctx.push("_ctx.");
ctx.push(&slot_name);
ctx.push("]");
}
} else if is_valid_js_identifier(&slot_name) {
ctx.push(&slot_name);
} else {
ctx.push("\"");
ctx.push(&escape_js_string(&slot_name));
ctx.push("\"");
}
if slot_name.as_str() == "default" {
has_generated_default = true;
}
ctx.push(": ");
ctx.use_helper(RuntimeHelper::WithCtx);
ctx.push(ctx.helper(RuntimeHelper::WithCtx));
ctx.push("(");
let params = if let Some(props_str) = get_slot_props(slot_dir) {
let processed = prefix_slot_defaults(&props_str);
ctx.push("(");
ctx.push(&processed);
ctx.push(")");
extract_slot_params(&props_str)
} else {
ctx.push("()");
vec![]
};
ctx.add_slot_params(¶ms);
ctx.push(" => [");
ctx.indent();
generate_slot_children(ctx, &template_el.children);
ctx.deindent();
ctx.newline();
ctx.push("])");
ctx.remove_slot_params(¶ms);
}
}
}
}
let default_children: Vec<_> = el
.children
.iter()
.filter(|child| {
if let TemplateChildNode::Element(template_el) = child {
!(template_el.tag.as_str() == "template" && has_v_slot(template_el))
} else {
true
}
})
.collect();
if !default_children.is_empty() && !has_generated_default {
if !first_slot {
ctx.push(",");
}
ctx.newline();
ctx.push("default: ");
ctx.use_helper(RuntimeHelper::WithCtx);
ctx.push(ctx.helper(RuntimeHelper::WithCtx));
ctx.push("(() => [");
ctx.indent();
for (i, child) in default_children.iter().enumerate() {
if i > 0 {
ctx.push(",");
}
ctx.newline();
generate_slot_child_node(ctx, child);
}
ctx.deindent();
ctx.newline();
ctx.push("])");
}
}
ctx.push(",");
ctx.newline();
if has_forwarded_slots {
ctx.push("_: 3 /* FORWARDED */");
} else if has_dynamic_slots {
ctx.push("_: 2 /* DYNAMIC */");
} else {
ctx.push("_: 1 /* STABLE */");
}
ctx.deindent();
ctx.newline();
ctx.push("}");
}
fn generate_create_slots(ctx: &mut CodegenContext, el: &ElementNode<'_>) {
ctx.use_helper(RuntimeHelper::CreateSlots);
ctx.push(ctx.helper(RuntimeHelper::CreateSlots));
ctx.push("({ _: 2 /* DYNAMIC */ }, [");
ctx.indent();
let mut first = true;
for child in &el.children {
match child {
TemplateChildNode::If(if_node) => {
if !first {
ctx.push(",");
}
first = false;
ctx.newline();
generate_conditional_slot(ctx, if_node);
}
TemplateChildNode::For(for_node) => {
if !first {
ctx.push(",");
}
first = false;
ctx.newline();
generate_looped_slot(ctx, for_node);
}
TemplateChildNode::Element(template_el) => {
if template_el.tag.as_str() == "template" && has_v_slot(template_el) {
if !first {
ctx.push(",");
}
first = false;
ctx.newline();
generate_static_slot_entry(ctx, template_el);
}
}
_ => {}
}
}
ctx.deindent();
ctx.newline();
ctx.push("])");
}
fn generate_conditional_slot(ctx: &mut CodegenContext, if_node: &IfNode<'_>) {
for (i, branch) in if_node.branches.iter().enumerate() {
if i > 0 {
ctx.newline();
ctx.push(": ");
}
if let Some(condition) = &branch.condition {
ctx.push("(");
generate_expression(ctx, condition);
ctx.push(")");
ctx.indent();
ctx.newline();
ctx.push("? ");
}
let slot_template = branch.children.iter().find_map(|child| {
if let TemplateChildNode::Element(el) = child {
if el.tag.as_str() == "template" && has_v_slot(el) {
return Some(el.as_ref());
}
}
None
});
if let Some(template_el) = slot_template {
generate_slot_object_entry(ctx, template_el, Some(i));
} else {
ctx.push("undefined");
}
if branch.condition.is_some() {
ctx.deindent();
}
}
if if_node
.branches
.last()
.is_none_or(|branch| branch.condition.is_some())
{
ctx.newline();
ctx.push(": undefined");
}
}
fn generate_looped_slot(ctx: &mut CodegenContext, for_node: &ForNode<'_>) {
ctx.use_helper(RuntimeHelper::RenderList);
ctx.push(ctx.helper(RuntimeHelper::RenderList));
ctx.push("(");
generate_expression(ctx, &for_node.source);
ctx.push(", (");
let mut callback_params: Vec<String> = Vec::new();
if let Some(value) = &for_node.value_alias {
generate_expression(ctx, value);
super::v_for::helpers::extract_for_params(value, &mut callback_params);
}
if let Some(key) = &for_node.key_alias {
ctx.push(", ");
generate_expression(ctx, key);
}
if let Some(index) = &for_node.object_index_alias {
ctx.push(", ");
generate_expression(ctx, index);
}
ctx.add_slot_params(&callback_params);
ctx.push(") => {");
ctx.indent();
ctx.newline();
ctx.push("return ");
let slot_template = for_node.children.iter().find_map(|child| {
if let TemplateChildNode::Element(el) = child {
if el.tag.as_str() == "template" && has_v_slot(el) {
return Some(el.as_ref());
}
}
None
});
if let Some(template_el) = slot_template {
generate_slot_object_entry(ctx, template_el, None);
}
ctx.remove_slot_params(&callback_params);
ctx.deindent();
ctx.newline();
ctx.push("})");
}
fn generate_slot_object_entry(
ctx: &mut CodegenContext,
template_el: &ElementNode<'_>,
key_index: Option<usize>,
) {
let slot_dir = template_el.props.iter().find_map(|p| {
if let PropNode::Directive(dir) = p {
if dir.name.as_str() == "slot" {
return Some(dir.as_ref());
}
}
None
});
if let Some(dir) = slot_dir {
let slot_name = get_slot_name(dir);
ctx.push("{");
ctx.indent();
ctx.newline();
ctx.push("name: \"");
ctx.push(&escape_js_string(&slot_name));
ctx.push("\",");
ctx.newline();
ctx.push("fn: ");
ctx.use_helper(RuntimeHelper::WithCtx);
ctx.push(ctx.helper(RuntimeHelper::WithCtx));
ctx.push("(");
let params = if let Some(props_str) = get_slot_props(dir) {
let processed = prefix_slot_defaults(&props_str);
ctx.push("(");
ctx.push(&processed);
ctx.push(")");
extract_slot_params(&props_str)
} else {
ctx.push("()");
vec![]
};
ctx.add_slot_params(¶ms);
ctx.push(" => [");
ctx.indent();
generate_slot_children(ctx, &template_el.children);
ctx.deindent();
ctx.newline();
ctx.push("])");
ctx.remove_slot_params(¶ms);
if let Some(key) = key_index {
ctx.push(",");
ctx.newline();
ctx.push("key: \"");
ctx.push(&key.to_compact_string());
ctx.push("\"");
}
ctx.deindent();
ctx.newline();
ctx.push("}");
}
}
fn generate_static_slot_entry(ctx: &mut CodegenContext, template_el: &ElementNode<'_>) {
generate_slot_object_entry(ctx, template_el, None);
}
fn generate_slot_children(ctx: &mut CodegenContext, children: &[TemplateChildNode<'_>]) {
let all_text_or_interp = children.iter().all(|child| {
matches!(
child,
TemplateChildNode::Text(_) | TemplateChildNode::Interpolation(_)
)
});
if all_text_or_interp && !children.is_empty() {
ctx.newline();
ctx.use_helper(RuntimeHelper::CreateText);
ctx.push(ctx.helper(RuntimeHelper::CreateText));
ctx.push("(");
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::Text(text) => {
ctx.push("\"");
ctx.push(&super::helpers::escape_js_string(&text.content));
ctx.push("\"");
}
TemplateChildNode::Interpolation(interp) => {
ctx.use_helper(RuntimeHelper::ToDisplayString);
ctx.push(ctx.helper(RuntimeHelper::ToDisplayString));
ctx.push("(");
generate_slot_expression(ctx, &interp.content);
ctx.push(")");
}
_ => {}
}
}
if has_interpolation {
ctx.push(", 1 /* TEXT */)");
} else {
ctx.push(")");
}
} else {
for (i, child) in children.iter().enumerate() {
if i > 0 {
ctx.push(",");
}
ctx.newline();
generate_slot_child_node(ctx, child);
}
}
}
fn generate_slot_child_node(ctx: &mut CodegenContext, child: &TemplateChildNode<'_>) {
match child {
TemplateChildNode::Text(text) => {
ctx.use_helper(RuntimeHelper::CreateText);
ctx.push(ctx.helper(RuntimeHelper::CreateText));
ctx.push("(\"");
ctx.push(&super::helpers::escape_js_string(&text.content));
ctx.push("\")");
}
TemplateChildNode::Interpolation(interp) => {
ctx.use_helper(RuntimeHelper::CreateText);
ctx.use_helper(RuntimeHelper::ToDisplayString);
ctx.push(ctx.helper(RuntimeHelper::CreateText));
ctx.push("(");
ctx.push(ctx.helper(RuntimeHelper::ToDisplayString));
ctx.push("(");
generate_slot_expression(ctx, &interp.content);
ctx.push("), 1 /* TEXT */)");
}
_ => {
generate_node(ctx, child);
}
}
}
fn generate_slot_expression(ctx: &mut CodegenContext, expr: &ExpressionNode<'_>) {
match expr {
ExpressionNode::Simple(exp) => {
if exp.is_static {
ctx.push("\"");
ctx.push(&exp.content);
ctx.push("\"");
} else {
let content = strip_ctx_prefix_for_slot_params(ctx, &exp.content);
ctx.push(&content);
}
}
ExpressionNode::Compound(comp) => {
for child in comp.children.iter() {
match child {
crate::ast::CompoundExpressionChild::Simple(exp) => {
if exp.is_static {
ctx.push("\"");
ctx.push(&exp.content);
ctx.push("\"");
} else {
let content = strip_ctx_prefix_for_slot_params(ctx, &exp.content);
ctx.push(&content);
}
}
crate::ast::CompoundExpressionChild::String(s) => {
ctx.push(s);
}
crate::ast::CompoundExpressionChild::Symbol(helper) => {
ctx.push(ctx.helper(*helper));
}
_ => {}
}
}
}
}
}
fn strip_ctx_prefix_for_slot_params(ctx: &CodegenContext, content: &str) -> String {
let mut result = String::new(content);
for param in &ctx.slot_params {
let mut prefixed = String::with_capacity(5 + param.len());
prefixed.push_str("_ctx.");
prefixed.push_str(param);
let replaced = result.replace(prefixed.as_str(), param.as_str());
result = String::from(replaced);
}
result
}
#[cfg(test)]
mod tests {
use super::{is_valid_js_identifier, prefix_slot_defaults};
#[test]
fn test_is_valid_js_identifier_valid() {
assert!(is_valid_js_identifier("foo"));
assert!(is_valid_js_identifier("_bar"));
assert!(is_valid_js_identifier("$baz"));
assert!(is_valid_js_identifier("foo123"));
assert!(is_valid_js_identifier("camelCase"));
assert!(is_valid_js_identifier("PascalCase"));
}
#[test]
fn test_is_valid_js_identifier_invalid() {
assert!(!is_valid_js_identifier("123foo")); assert!(!is_valid_js_identifier("")); assert!(!is_valid_js_identifier("foo-bar")); assert!(!is_valid_js_identifier("foo.bar")); assert!(!is_valid_js_identifier("foo bar")); assert!(!is_valid_js_identifier("item-header")); }
#[test]
fn test_hyphenated_slot_names_need_quotes() {
assert!(!is_valid_js_identifier("item-header"));
assert!(!is_valid_js_identifier("card-body"));
assert!(!is_valid_js_identifier("main-content"));
assert!(!is_valid_js_identifier("list-item"));
}
#[test]
fn test_regular_slot_names_are_valid_identifiers() {
assert!(is_valid_js_identifier("default"));
assert!(is_valid_js_identifier("header"));
assert!(is_valid_js_identifier("footer"));
assert!(is_valid_js_identifier("content"));
}
#[test]
fn test_prefix_slot_defaults() {
assert_eq!(
prefix_slot_defaults("{ item = defaultItem }"),
"{ item = _ctx.defaultItem }"
);
assert_eq!(prefix_slot_defaults("{ count = 0 }"), "{ count = 0 }");
assert_eq!(
prefix_slot_defaults("{ name = 'test' }"),
"{ name = 'test' }"
);
assert_eq!(prefix_slot_defaults("{ x = true }"), "{ x = true }");
assert_eq!(prefix_slot_defaults("{ x = false }"), "{ x = false }");
assert_eq!(prefix_slot_defaults("{ x = null }"), "{ x = null }");
assert_eq!(
prefix_slot_defaults("{ x = undefined }"),
"{ x = undefined }"
);
}
}