use vize_atelier_vapor::{compile_vapor, VaporCompilerOptions};
use vize_carton::Bump;
use crate::types::*;
pub(crate) fn compile_template_block(
template: &SfcTemplateBlock,
options: &TemplateCompileOptions,
scope_id: &str,
has_scoped: bool,
is_ts: bool,
bindings: Option<&BindingMetadata>,
croquis: Option<vize_croquis::analysis::Croquis>,
) -> Result<String, SfcError> {
let allocator = Bump::new();
let mut dom_opts = options.compiler_options.clone().unwrap_or_default();
dom_opts.mode = vize_atelier_core::options::CodegenMode::Module;
dom_opts.prefix_identifiers = true;
dom_opts.scope_id = if has_scoped {
let mut attr = String::with_capacity(scope_id.len() + 7);
attr.push_str("data-v-");
attr.push_str(scope_id);
Some(attr.into())
} else {
None
};
dom_opts.ssr = options.ssr;
dom_opts.is_ts = is_ts;
if bindings.is_some() {
dom_opts.inline = true;
dom_opts.hoist_static = true;
dom_opts.cache_handlers = true;
}
dom_opts.binding_metadata = bindings.cloned();
if let Some(c) = croquis {
dom_opts.croquis = Some(Box::new(c));
}
let (_, errors, result) =
vize_atelier_dom::compile_template_with_options(&allocator, &template.content, dom_opts);
if !errors.is_empty() {
let mut message = String::from("Template compilation errors: ");
use std::fmt::Write as _;
let _ = write!(&mut message, "{:?}", errors);
return Err(SfcError {
message,
code: Some("TEMPLATE_ERROR".to_string()),
loc: Some(template.loc.clone()),
});
}
let mut output = String::new();
output.push_str(&result.preamble);
output.push('\n');
output.push_str(&result.code);
output.push('\n');
Ok(output)
}
pub(crate) fn compile_template_block_vapor(
template: &SfcTemplateBlock,
scope_id: &str,
has_scoped: bool,
) -> Result<String, SfcError> {
let allocator = Bump::new();
let vapor_opts = VaporCompilerOptions {
prefix_identifiers: false,
ssr: false,
..Default::default()
};
let result = compile_vapor(&allocator, &template.content, vapor_opts);
if !result.error_messages.is_empty() {
let mut message = String::from("Vapor template compilation errors: ");
use std::fmt::Write as _;
let _ = write!(&mut message, "{:?}", result.error_messages);
return Err(SfcError {
message,
code: Some("VAPOR_TEMPLATE_ERROR".to_string()),
loc: Some(template.loc.clone()),
});
}
let mut output = String::new();
let scope_attr = if has_scoped {
let mut attr = String::with_capacity(scope_id.len() + 7);
attr.push_str("data-v-");
attr.push_str(scope_id);
attr
} else {
String::new()
};
let code = &result.code;
if let Some(import_end) = code.find('\n') {
let import_line = &code[..import_end];
output.push_str(import_line);
output.push('\n');
let rest = &code[import_end + 1..];
let mut template_decls = Vec::new();
let mut func_start = 0;
for (i, line) in rest.lines().enumerate() {
if line.starts_with("const t") && line.contains("_template(") {
if has_scoped && !scope_attr.is_empty() {
let modified = add_scope_id_to_template(line, &scope_attr);
template_decls.push(modified);
} else {
template_decls.push(line.to_string());
}
} else if line.starts_with("export default") {
func_start = i;
break;
}
}
for decl in template_decls {
output.push_str(&decl);
output.push('\n');
}
let lines: Vec<&str> = rest.lines().collect();
if func_start < lines.len() {
output.push_str("function render(_ctx, $props, $emit, $attrs, $slots) {\n");
for line in lines.iter().skip(func_start + 1) {
if *line == "}" {
break;
}
output.push_str(line);
output.push('\n');
}
output.push_str("}\n");
}
}
Ok(output)
}
fn add_scope_id_to_template(template_line: &str, scope_id: &str) -> String {
if let Some(start) = template_line.find("\"<") {
if let Some(end) = template_line.rfind(">\"") {
let prefix = &template_line[..start + 2]; let content = &template_line[start + 2..end + 1]; let suffix = &template_line[end + 1..];
if let Some(tag_end) = content.find(|c: char| c.is_whitespace() || c == '>') {
let tag_name = &content[..tag_end];
let rest = &content[tag_end..];
let mut result = String::with_capacity(
prefix.len() + tag_name.len() + scope_id.len() + rest.len() + suffix.len() + 1,
);
result.push_str(prefix);
result.push_str(tag_name);
result.push(' ');
result.push_str(scope_id);
result.push_str(rest);
result.push_str(suffix);
return result;
}
}
}
template_line.to_string()
}
fn count_braces_outside_strings(line: &str) -> i32 {
let mut count: i32 = 0;
let mut in_string = false;
let mut string_char = '\0';
let mut escape = false;
for ch in line.chars() {
if escape {
escape = false;
continue;
}
if ch == '\\' && in_string {
escape = true;
continue;
}
match ch {
'\'' | '"' | '`' => {
if !in_string {
in_string = true;
string_char = ch;
} else if ch == string_char {
in_string = false;
}
}
'{' if !in_string => count += 1,
'}' if !in_string => count -= 1,
_ => {}
}
}
count
}
#[allow(dead_code)]
fn compact_render_body(render_body: &str) -> String {
let mut result = String::new();
let mut chars = render_body.chars().peekable();
let mut paren_depth: i32 = 0;
let mut bracket_depth: i32 = 0;
let mut brace_depth: i32 = 0;
let mut in_string = false;
let mut string_char = '\0';
let mut in_template = false;
while let Some(ch) = chars.next() {
match ch {
'"' | '\'' if !in_template => {
if !in_string {
in_string = true;
string_char = ch;
} else if string_char == ch {
in_string = false;
}
result.push(ch);
}
'`' => {
in_template = !in_template;
result.push(ch);
}
'(' if !in_string && !in_template => {
paren_depth += 1;
result.push(ch);
}
')' if !in_string && !in_template => {
paren_depth = paren_depth.saturating_sub(1);
result.push(ch);
}
'[' if !in_string && !in_template => {
bracket_depth += 1;
result.push(ch);
}
']' if !in_string && !in_template => {
bracket_depth = bracket_depth.saturating_sub(1);
result.push(ch);
}
'{' if !in_string && !in_template => {
brace_depth += 1;
result.push(ch);
}
'}' if !in_string && !in_template => {
brace_depth = brace_depth.saturating_sub(1);
result.push(ch);
}
'\n' => {
if brace_depth > 0 && !in_string && !in_template {
result.push('\n');
} else if (paren_depth > 0 || bracket_depth > 0) && !in_string && !in_template {
result.push(' ');
while let Some(&next_ch) = chars.peek() {
if next_ch.is_whitespace() && next_ch != '\n' {
chars.next();
} else {
break;
}
}
} else {
result.push(ch);
}
}
_ => result.push(ch),
}
}
result
}
pub(crate) fn extract_template_parts_full(template_code: &str) -> (String, String, String) {
let mut imports = String::new();
let mut hoisted = String::new();
let mut render_fn = String::new();
let mut in_render = false;
let mut brace_depth = 0;
for line in template_code.lines() {
let trimmed = line.trim();
if trimmed.starts_with("import ") {
imports.push_str(line);
imports.push('\n');
} else if trimmed.starts_with("const _hoisted_") {
hoisted.push_str(line);
hoisted.push('\n');
} else if trimmed.starts_with("export function render(")
|| trimmed.starts_with("function render(")
{
in_render = true;
brace_depth = 0;
brace_depth += count_braces_outside_strings(line);
render_fn.push_str(line);
render_fn.push('\n');
} else if in_render {
brace_depth += count_braces_outside_strings(line);
render_fn.push_str(line);
render_fn.push('\n');
if brace_depth == 0 {
in_render = false;
}
}
}
(imports, hoisted, render_fn)
}
#[allow(dead_code)]
pub(crate) fn extract_template_parts(template_code: &str) -> (String, String, String, String) {
let mut imports = String::new();
let mut hoisted = String::new();
let mut preamble = String::new(); let mut render_body = String::new();
let mut in_render = false;
let mut in_return = false;
let mut brace_depth = 0;
let mut return_paren_depth = 0;
let lines: Vec<&str> = template_code.lines().collect();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("import ") {
imports.push_str(line);
imports.push('\n');
} else if trimmed.starts_with("const _hoisted_") {
hoisted.push_str(line);
hoisted.push('\n');
} else if trimmed.starts_with("export function render(")
|| trimmed.starts_with("function render(")
{
in_render = true;
brace_depth = 0;
brace_depth += count_braces_outside_strings(line);
} else if in_render {
brace_depth += count_braces_outside_strings(line);
if in_return {
render_body.push('\n');
render_body.push_str(line);
return_paren_depth += line.matches('(').count() as i32;
return_paren_depth -= line.matches(')').count() as i32;
if return_paren_depth <= 0 {
let next_continues_ternary = lines
.iter()
.skip(i + 1)
.map(|l| l.trim())
.find(|l| !l.is_empty())
.map(|l| l.starts_with('?') || l.starts_with(':'))
.unwrap_or(false);
if !next_continues_ternary {
in_return = false;
let trimmed_body = render_body.trim_end();
if let Some(stripped) = trimmed_body.strip_suffix(';') {
render_body = stripped.to_string();
}
}
}
} else if let Some(stripped) = trimmed.strip_prefix("return ") {
render_body = stripped.to_string();
return_paren_depth =
stripped.matches('(').count() as i32 - stripped.matches(')').count() as i32;
if return_paren_depth > 0 {
in_return = true;
} else {
let next_continues_ternary = lines
.iter()
.skip(i + 1)
.map(|l| l.trim())
.find(|l| !l.is_empty())
.map(|l| l.starts_with('?') || l.starts_with(':'))
.unwrap_or(false);
if next_continues_ternary {
in_return = true;
} else {
if render_body.ends_with(';') {
render_body.pop();
}
}
}
} else if trimmed.starts_with("const _component_")
|| trimmed.starts_with("const _directive_")
{
preamble.push_str(trimmed);
preamble.push('\n');
}
if brace_depth == 0 {
in_render = false;
}
}
}
let compacted = compact_render_body(&render_body);
(imports, hoisted, preamble, compacted)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_scope_id_to_template() {
let input = r#"const t0 = _template("<div class='container'>Hello</div>")"#;
let result = add_scope_id_to_template(input, "data-v-abc123");
assert!(result.contains("data-v-abc123"));
}
#[test]
fn test_count_braces_normal() {
assert_eq!(count_braces_outside_strings("{ a: 1 }"), 0);
assert_eq!(count_braces_outside_strings("{"), 1);
assert_eq!(count_braces_outside_strings("}"), -1);
assert_eq!(count_braces_outside_strings("{ { }"), 1);
}
#[test]
fn test_count_braces_ignores_string_braces() {
assert_eq!(
count_braces_outside_strings("_toDisplayString(isArray.value ? ']' : '}')"),
0
);
assert_eq!(count_braces_outside_strings(r#"var x = "{";"#), 0);
assert_eq!(count_braces_outside_strings("var x = `{`;"), 0);
}
#[test]
fn test_count_braces_mixed_string_and_code() {
assert_eq!(count_braces_outside_strings("if (x) { var s = '}'"), 1);
}
#[test]
fn test_count_braces_escaped_quotes() {
assert_eq!(count_braces_outside_strings(r"var x = '\'' + '}'"), 0);
}
#[test]
fn test_extract_template_parts_full_brace_in_string() {
let template_code = r#"import { toDisplayString as _toDisplayString } from 'vue'
export function render(_ctx, _cache) {
return _toDisplayString(isArray.value ? ']' : '}')
}"#;
let (imports, _hoisted, render_fn) = extract_template_parts_full(template_code);
assert!(imports.contains("import"));
assert!(
render_fn.contains("_toDisplayString"),
"Render function was truncated. Got:\n{}",
render_fn
);
let trimmed = render_fn.trim();
assert!(
trimmed.ends_with('}'),
"Render function should end with closing brace. Got:\n{}",
render_fn
);
}
#[test]
fn test_extract_template_parts_basic() {
let template_code = r#"import { createVNode as _createVNode } from 'vue'
const _hoisted_1 = { class: "test" }
export function render(_ctx, _cache) {
return _createVNode("div", _hoisted_1, "Hello")
}"#;
let (imports, hoisted, _preamble, render_body) = extract_template_parts(template_code);
assert!(imports.contains("import"));
assert!(hoisted.contains("_hoisted_1"));
assert!(render_body.contains("_createVNode"));
}
}