use crate::template::*;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::str::FromStr;
#[test]
fn test_at_interpolation_glue() {
let input = quote! {
make@{name_ident}
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("\"make\""));
assert!(
s.contains("name_ident . to_string ()"),
"Expected generated code to include name_ident.to_string()"
);
assert!(
s.contains("__out . push_str (\"make\") ;"),
"Generated code should push \"make\""
);
assert!(
s.contains("__out . push_str (& name_ident . to_string ()) ;"),
"Generated code should push interpolated name_ident"
);
}
#[test]
fn test_let_scope() {
let input = TokenStream2::from_str(
r###" {$let val_local = val}
@{val_local}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("let val_local = val"));
assert!(s.contains("val_local . to_string"));
}
#[test]
fn test_for_loop() {
let input = TokenStream2::from_str(
r###" {#for item in items}
@{item}
{/for}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("for item in items"), "Should generate for loop");
assert!(s.contains("item . to_string"), "Should interpolate item");
}
#[test]
fn test_if_else() {
let input = TokenStream2::from_str(
r###" {#if condition}
"true"
{:else}
"false"
{/if}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("if condition"), "Should generate if condition");
assert!(s.contains("else"), "Should have else branch");
}
#[test]
fn test_if_else_if() {
let input = TokenStream2::from_str(
r###" {#if a}
"a"
{:else if b}
"b"
{:else}
"c"
{/if}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("if a"), "Should have if a");
assert!(s.contains("if b"), "Should have else if b");
assert!(s.contains("else"), "Should have else");
}
#[test]
fn test_string_interpolation_simple() {
let input = quote! {
"Hello @{name}!"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("push_str (\"\\\"\")"), "Should push opening quote");
assert!(s.contains("\"Hello \""), "Should push 'Hello '");
assert!(s.contains("name . to_string"), "Should interpolate name");
assert!(s.contains("\"!\""), "Should push '!'");
}
#[test]
fn test_string_no_interpolation() {
let input = quote! {
"Just a plain string"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("Just a plain string"),
"Should contain the string content"
);
}
#[test]
fn test_string_interpolation_multiple() {
let input = quote! {
"@{greeting}, @{name}!"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("greeting . to_string"),
"Should interpolate greeting"
);
assert!(s.contains("name . to_string"), "Should interpolate name");
}
#[test]
fn test_string_interpolation_with_method_call() {
let input = quote! {
"Name: @{name.to_uppercase()}"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("to_uppercase"), "Should contain method call");
}
#[test]
fn test_backtick_template_simple() {
let input = quote! {
"'^hello ${name}^'"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("\"`\""), "Should push opening backtick");
assert!(
s.contains("hello ${name}"),
"Should contain template content"
);
}
#[test]
fn test_backtick_template_with_rust_interpolation() {
let input = quote! {
"'^hello @{rust_var}^'"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("\"`\""), "Should push backtick");
assert!(
s.contains("rust_var . to_string"),
"Should interpolate Rust var"
);
}
#[test]
fn test_backtick_template_mixed() {
let input = quote! {
"'^${js_var} and @{rust_var}^'"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("${js_var}"),
"Should pass through JS interpolation"
);
assert!(
s.contains("rust_var . to_string"),
"Should interpolate Rust var"
);
}
#[test]
fn test_at_symbol_without_brace_passes_through() {
let input = quote! {
"email@domain.com"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("email@domain.com"),
"Should pass through @ unchanged"
);
}
#[test]
fn test_at_symbol_in_backtick_passes_through() {
let input = quote! {
"'^email@domain.com^'"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("\"`\""), "Should push backtick");
assert!(
s.contains("email@domain.com"),
"Should pass through @ unchanged"
);
}
#[test]
fn test_escape_at_before_brace() {
let input = quote! {
"use @@{decorators}"
};
let output = parse_template(input);
let s = output.unwrap().to_string();
let expected = "@{decorators}";
assert!(
s.contains(expected),
"Should contain literal @{{decorators}}"
);
assert!(
!s.contains("decorators . to_string"),
"Should not interpolate"
);
}
#[test]
fn test_if_let_simple() {
let input = TokenStream2::from_str(
r###" {#if let Some(value) = option}
@{value}
{/if}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("if let Some (value)"),
"Should have if let pattern"
);
assert!(s.contains("= option"), "Should have expression");
}
#[test]
fn test_if_let_with_else() {
let input = TokenStream2::from_str(
r###" {#if let Some(x) = maybe}
"found"
{:else}
"not found"
{/if}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("if let Some (x)"), "Should have if let pattern");
assert!(s.contains("else"), "Should have else branch");
}
#[test]
fn test_match_simple() {
let input = TokenStream2::from_str(
r###" {#match value}
{:case Some(x)}
@{x}
{:case None}
"nothing"
{/match}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("match value"), "Should have match expr");
assert!(s.contains("Some (x) =>"), "Should have Some case arm");
assert!(s.contains("None =>"), "Should have None case arm");
}
#[test]
fn test_match_with_wildcard() {
let input = TokenStream2::from_str(
r###" {#match num}
{:case 1}
"one"
{:case 2}
"two"
{:case _}
"other"
{/match}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("match num"), "Should have match expr");
assert!(s.contains("1 =>"), "Should have case 1");
assert!(s.contains("2 =>"), "Should have case 2");
assert!(s.contains("_ =>"), "Should have wildcard case");
}
#[test]
fn test_match_with_interpolation() {
let input = TokenStream2::from_str(
r###" {#match result}
{:case Ok(val)}
"success: @{val}"
{:case Err(e)}
"error: @{e}"
{/match}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("val . to_string"), "Should interpolate val");
assert!(s.contains("e . to_string"), "Should interpolate e");
}
#[test]
fn test_ident_block_basic() {
let input = TokenStream2::from_str(r#"{|namespace@{suffix}|}"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("suffix . to_string"), "Should interpolate suffix");
assert!(
s.contains(r#"__out . push_str ("namespace")"#),
"Should emit namespace without space"
);
}
#[test]
fn test_ident_block_multiple_parts() {
let input = TokenStream2::from_str(r#"{|@{a}|}{|@{b}|}{|@{c}|}"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("a . to_string"), "Should interpolate a");
assert!(s.contains("b . to_string"), "Should interpolate b");
assert!(s.contains("c . to_string"), "Should interpolate c");
}
#[test]
fn test_ident_block_with_surrounding_text() {
let input = TokenStream2::from_str(r#"function {|get@{name}|}() { }"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains(r#""function""#), "Should have function keyword");
assert!(s.contains("name . to_string"), "Should interpolate name");
assert!(s.contains(r#"__out . push_str ("get")"#), "Should have get prefix");
}
#[test]
fn test_ident_block_empty() {
let input = TokenStream2::from_str(r#"prefix{||}suffix"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains(r#""prefix""#), "Should have prefix");
assert!(s.contains(r#""suffix""#), "Should have suffix");
}
#[test]
fn test_ident_block_plain_text_only() {
let input = TokenStream2::from_str(r#"{|hello|}"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains(r#"__out . push_str ("hello")"#), "Should emit hello");
}
#[test]
fn test_while_loop() {
let input = TokenStream2::from_str(
r###"
{#while i < 5}
item @{i}
{/while}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("while i < 5"), "Should generate while loop");
}
#[test]
fn test_while_let_loop() {
let input = TokenStream2::from_str(
r###"
{#while let Some(item) = iter.next()}
@{item}
{/while}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("while let Some (item) = iter . next ()"),
"Should generate while-let: {}",
s
);
}
#[test]
fn test_while_with_complex_condition() {
let input = TokenStream2::from_str(
r###"
{#while !done && count < max}
processing
{/while}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("while ! done && count < max"),
"Should handle complex condition: {}",
s
);
}
#[test]
fn test_let_mut() {
let input = TokenStream2::from_str(
r###"
{$let mut count = 0}
count is @{count}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("let mut count = 0"),
"Should have mutable binding: {}",
s
);
}
#[test]
fn test_let_mut_with_type() {
let input = TokenStream2::from_str(
r###"
{$let mut items: Vec<String> = Vec::new()}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("let mut items : Vec < String > = Vec :: new ()"),
"Should handle typed mutable binding: {}",
s
);
}
#[test]
fn test_do_side_effect() {
let input = TokenStream2::from_str(
r###"
{$do results.push("item".to_string())}
{$do counter += 1}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("results . push"),
"Should have push side effect: {}",
s
);
assert!(s.contains("counter += 1"), "Should have increment: {}", s);
}
#[test]
fn test_do_method_call() {
let input = TokenStream2::from_str(
r###"
{$do vec.clear()}
{$do map.insert(key, value)}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("vec . clear ()"), "Should have clear call: {}", s);
assert!(
s.contains("map . insert (key , value)"),
"Should have insert call: {}",
s
);
}
#[test]
fn test_while_with_mut_and_do() {
let input = TokenStream2::from_str(
r###"
{$let mut i = 0}
{#while i < 5}
item @{i}
{$do i += 1}
{/while}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains("let mut i = 0"), "Should have mutable let: {}", s);
assert!(s.contains("while i < 5"), "Should generate while loop: {}", s);
assert!(s.contains("i += 1"), "Should have do expression: {}", s);
}
#[test]
fn test_while_with_break_condition() {
let input = TokenStream2::from_str(
r###"
{$let mut found = false}
{#while !found}
{#if condition}
{$do found = true}
found it
{/if}
{/while}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains("while ! found"),
"Should have while with negation: {}",
s
);
assert!(
s.contains("found = true"),
"Should have assignment: {}",
s
);
}
#[test]
fn test_nested_while_loops() {
let input = TokenStream2::from_str(
r###"
{#while outer_cond}
outer
{#while inner_cond}
inner
{/while}
{/while}
"###,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
let while_count = s.matches("while").count();
assert!(
while_count >= 2,
"Should have at least 2 while loops: {}",
s
);
}
#[test]
fn test_block_comment_simple() {
let input = TokenStream2::from_str(r#"{> This is a comment <}"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains(r#""/* ""#), "Should have opening comment: {}", s);
assert!(s.contains(r#"" */""#), "Should have closing comment: {}", s);
assert!(
s.contains(r#"__out . push_str ("This")"#),
"Should contain 'This': {}",
s
);
}
#[test]
fn test_doc_comment_simple() {
let input = TokenStream2::from_str(r#"{>> This is JSDoc <<}"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains(r#""/** ""#),
"Should have opening doc comment: {}",
s
);
assert!(s.contains(r#"" */""#), "Should have closing comment: {}", s);
assert!(
s.contains(r#"__out . push_str ("This")"#),
"Should contain 'This': {}",
s
);
}
#[test]
fn test_comment_with_interpolation() {
let input = TokenStream2::from_str(r#"{> Generated by @{author} <}"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains(r#""/* ""#), "Should have opening comment: {}", s);
assert!(
s.contains("author . to_string"),
"Should interpolate author: {}",
s
);
assert!(s.contains(r#"" */""#), "Should have closing comment: {}", s);
}
#[test]
fn test_doc_comment_with_jsdoc_tags() {
let input = TokenStream2::from_str(r#"{>> @param name The name <<}"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains(r#""/** ""#),
"Should have opening doc comment: {}",
s
);
assert!(s.contains("@"), "Should contain @ symbol: {}", s);
assert!(
s.contains(r#"__out . push_str ("param")"#),
"Should contain 'param': {}",
s
);
}
#[test]
fn test_empty_block_comment() {
let input = TokenStream2::from_str(r#"{><}"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(s.contains(r#""/* ""#), "Should have opening comment: {}", s);
assert!(s.contains(r#"" */""#), "Should have closing comment: {}", s);
}
#[test]
fn test_empty_doc_comment() {
let input = TokenStream2::from_str(r#"{>><<}"#).unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains(r#""/** ""#),
"Should have opening doc comment: {}",
s
);
assert!(s.contains(r#"" */""#), "Should have closing comment: {}", s);
}
#[test]
fn test_comment_in_context() {
let input = TokenStream2::from_str(
r#"
{>> @param {string} name <<}
function greet(name) { }
"#,
)
.unwrap();
let output = parse_template(input);
let s = output.unwrap().to_string();
assert!(
s.contains(r#""/** ""#),
"Should have opening doc comment: {}",
s
);
assert!(
s.contains(r#""function""#),
"Should have function keyword: {}",
s
);
}