#![allow(clippy::disallowed_macros)]
use super::{
BindingMetadata, BindingType, DirectiveNode, ElementNode, ExpressionNode, ForNode, IfNode,
InterpolationNode, MappingData, PropNode, RootNode, SourceMapping, SourceRange,
TemplateChildNode, VirtualTsGenerator, VirtualTsOutput,
};
use vize_carton::{append, profile, String, ToCompactString};
impl VirtualTsGenerator {
pub fn generate_template(
&mut self,
ast: &RootNode,
bindings: &BindingMetadata,
block_offset: u32,
emit_context: bool,
) -> VirtualTsOutput {
self.reset();
self.block_offset = block_offset;
if emit_context {
self.write_line("// Virtual TypeScript for template type checking");
self.write_line("// Generated by vize_croquis");
self.write_line("");
self.write_line("// Context from script setup");
self.write("declare const __ctx: { ");
let binding_entries: Vec<_> = bindings.bindings.iter().collect();
for (i, (name, _)) in binding_entries.iter().enumerate() {
if i > 0 {
self.write(", ");
}
self.write(&format!("{}: any", name));
}
self.write_line(" };");
if !binding_entries.is_empty() {
self.write("const { ");
for (i, (name, _)) in binding_entries.iter().enumerate() {
if i > 0 {
self.write(", ");
}
self.write(name);
}
self.write_line(" } = __ctx;");
}
self.write_line("");
}
self.write_line("// Template expressions");
profile!(
"croquis.virtual_ts.template.visit_children",
self.visit_children(&ast.children)
);
profile!("croquis.virtual_ts.create_output", self.create_output())
}
pub(crate) fn emit_template_scope(&mut self, ast: &RootNode, bindings: &BindingMetadata) {
self.emit_line("");
self.emit_line("// Template scope (inherits from setup)");
self.emit_line("(function __template() {");
self.indent_level += 1;
profile!(
"croquis.virtual_ts.template.emit_ref_declarations",
self.emit_template_ref_declarations(bindings)
);
self.emit_line("// Template expressions");
profile!(
"croquis.virtual_ts.template.visit_children",
self.visit_children(&ast.children)
);
self.indent_level = self.indent_level.saturating_sub(1);
self.emit_line("})();");
}
fn emit_template_ref_declarations(&mut self, bindings: &BindingMetadata) {
let template_refs: Vec<_> = bindings
.bindings
.iter()
.filter(|(_, t)| matches!(t, BindingType::SetupRef))
.collect();
if !template_refs.is_empty() {
self.emit_line("// Template refs (auto-unwrapped)");
for (name, _) in template_refs {
self.emit_line(&format!("const __unwrapped_{} = {}.value;", name, name));
}
self.emit_line("");
}
}
pub(crate) fn visit_children(&mut self, children: &[TemplateChildNode]) {
for child in children {
profile!(
"croquis.virtual_ts.template.visit_child",
self.visit_child(child)
);
}
}
fn visit_child(&mut self, node: &TemplateChildNode) {
match node {
TemplateChildNode::Element(el) => {
profile!(
"croquis.virtual_ts.template.element",
self.visit_element(el)
)
}
TemplateChildNode::Interpolation(interp) => profile!(
"croquis.virtual_ts.template.interpolation",
self.visit_interpolation(interp)
),
TemplateChildNode::If(if_node) => {
profile!("croquis.virtual_ts.template.if", self.visit_if(if_node))
}
TemplateChildNode::For(for_node) => {
profile!("croquis.virtual_ts.template.for", self.visit_for(for_node))
}
TemplateChildNode::IfBranch(branch) => {
if let Some(ref cond) = branch.condition {
self.emit_expression(cond, "v-if");
}
self.visit_children(&branch.children);
}
_ => {}
}
}
fn visit_element(&mut self, element: &ElementNode) {
let v_for_exp = element.props.iter().find_map(|prop| {
if let PropNode::Directive(dir) = prop {
if dir.name == "for" {
return dir.exp.as_ref();
}
}
None
});
if let Some(exp) = v_for_exp {
self.emit_v_for_scope(exp, |this| {
for prop in &element.props {
if let PropNode::Directive(dir) = prop {
if dir.name != "for" {
profile!(
"croquis.virtual_ts.template.directive",
this.visit_directive(dir)
);
}
}
}
profile!(
"croquis.virtual_ts.template.element_children",
this.visit_children(&element.children)
);
});
} else {
for prop in &element.props {
if let PropNode::Directive(dir) = prop {
profile!(
"croquis.virtual_ts.template.directive",
self.visit_directive(dir)
);
}
}
profile!(
"croquis.virtual_ts.template.element_children",
self.visit_children(&element.children)
);
}
}
fn emit_v_for_scope<F>(&mut self, exp: &ExpressionNode, body: F)
where
F: FnOnce(&mut Self),
{
let content = match exp {
ExpressionNode::Simple(s) => s.content.as_str(),
ExpressionNode::Compound(c) => c.loc.source.as_str(),
};
if let Some(in_pos) = content.find(" in ").or_else(|| content.find(" of ")) {
let left = &content[..in_pos];
let right = &content[in_pos + 4..];
self.emit_line("// v-for scope");
self.emit_line("{");
self.indent_level += 1;
let vars_part = left.trim();
let vars_part = vars_part.trim_start_matches('(').trim_end_matches(')');
for var in vars_part.split(',') {
let var = var.trim();
if !var.is_empty()
&& var
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
{
self.emit_line(&format!("let {}: any;", var));
}
}
let source = right.trim();
let var_name = format!("__expr_{}", self.expr_counter);
self.expr_counter += 1;
self.emit_line(&format!("const {} = {};", var_name, source));
profile!("croquis.virtual_ts.template.v_for_body", body(self));
self.indent_level = self.indent_level.saturating_sub(1);
self.emit_line("}");
} else {
self.emit_expression(exp, "v-for");
body(self);
}
}
fn visit_directive(&mut self, directive: &DirectiveNode) {
if let Some(ref exp) = directive.exp {
self.emit_expression(exp, &directive.name);
}
}
fn visit_interpolation(&mut self, interp: &InterpolationNode) {
self.emit_expression(&interp.content, "interpolation");
}
fn visit_if(&mut self, if_node: &IfNode) {
for branch in &if_node.branches {
if let Some(ref cond) = branch.condition {
self.emit_expression(cond, "v-if");
}
self.visit_children(&branch.children);
}
}
fn visit_for(&mut self, for_node: &ForNode) {
let parse_result = &for_node.parse_result;
self.emit_line("// v-for scope");
self.emit_line("{");
self.indent_level += 1;
fn extract_var_name(expr: &ExpressionNode) -> Option<String> {
match expr {
ExpressionNode::Simple(simple) => {
let name = simple.content.trim();
if !name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
{
Some(name.to_compact_string())
} else {
None
}
}
ExpressionNode::Compound(compound) => {
use vize_relief::ast::CompoundExpressionChild;
if let Some(CompoundExpressionChild::Simple(simple)) = compound.children.first()
{
let name = simple.content.trim();
if !name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
{
return Some(name.to_compact_string());
}
}
None
}
}
}
if let Some(ref value) = parse_result.value {
if let Some(var_name) = extract_var_name(value) {
self.emit_line(&format!("let {}: any;", var_name));
}
}
if let Some(ref key) = parse_result.key {
if let Some(var_name) = extract_var_name(key) {
self.emit_line(&format!("let {}: any;", var_name));
}
}
if let Some(ref index) = parse_result.index {
if let Some(var_name) = extract_var_name(index) {
self.emit_line(&format!("let {}: any;", var_name));
}
}
if let Some(ref value_alias) = for_node.value_alias {
if let Some(var_name) = extract_var_name(value_alias) {
self.emit_line(&format!("let {}: any;", var_name));
}
}
if let Some(ref key_alias) = for_node.key_alias {
if let Some(var_name) = extract_var_name(key_alias) {
self.emit_line(&format!("let {}: any;", var_name));
}
}
if let Some(ref index_alias) = for_node.object_index_alias {
if let Some(var_name) = extract_var_name(index_alias) {
self.emit_line(&format!("let {}: any;", var_name));
}
}
let source_expr = &parse_result.source;
match source_expr {
ExpressionNode::Simple(simple) => {
if !simple.content.is_empty() {
let expr_index = self.expr_counter;
self.expr_counter += 1;
self.emit_generated_line(|output| {
append!(*output, "const __expr_{expr_index} = {};", simple.content);
});
}
}
ExpressionNode::Compound(_) => {
self.emit_expression(source_expr, "v-for source");
}
}
profile!(
"croquis.virtual_ts.template.for_children",
self.visit_children(&for_node.children)
);
self.indent_level = self.indent_level.saturating_sub(1);
self.emit_line("}");
}
pub(crate) fn emit_expression(&mut self, expr: &ExpressionNode, context: &str) {
profile!("croquis.virtual_ts.emit_expression", {
match expr {
ExpressionNode::Simple(simple) => {
if simple.content.is_empty() {
return;
}
let expr_index = self.expr_counter;
self.expr_counter += 1;
let prefix_len = self.indent_level * 2
+ "const ".len()
+ "__expr_".len()
+ decimal_len(expr_index)
+ " = ".len();
let expr_start = self.gen_offset + prefix_len as u32;
let expr_end = expr_start + simple.content.len() as u32;
let source_start = simple.loc.start.offset + self.block_offset;
let source_end = simple.loc.end.offset + self.block_offset;
self.mappings.push(SourceMapping::with_data(
SourceRange::new(source_start, source_end),
SourceRange::new(expr_start, expr_end),
MappingData::Expression {
text: simple.content.to_compact_string(),
},
));
self.emit_generated_line(|output| {
append!(*output, "const __expr_{expr_index} = {};", simple.content);
});
}
ExpressionNode::Compound(_) => {
let expr_index = self.expr_counter;
self.expr_counter += 1;
self.emit_generated_line(|output| {
append!(
*output,
"const __expr_{expr_index} = void 0 as any; // {context} compound"
);
});
}
}
});
}
}
fn decimal_len(mut value: u32) -> usize {
let mut len = 1;
while value >= 10 {
value /= 10;
len += 1;
}
len
}