use std::iter::once;
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::{Block, ExprBlock};
use self::{
ast::ToCode,
ctxt::{Ctx, prepare_vars},
input::QuoteInput,
ret_type::parse_input_type,
template::parse_template,
};
mod ast;
mod builder;
#[allow(dead_code)]
mod compiler;
mod ctxt;
mod input;
mod ret_type;
#[allow(dead_code)]
mod template;
#[cfg(test)]
mod test;
#[proc_macro]
pub fn ts_quote(input: TokenStream) -> TokenStream {
match ts_quote_impl(input.into()) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
fn ts_quote_impl(input: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
let QuoteInput {
src,
as_token: _,
output_type,
vars,
} = syn::parse2::<QuoteInput>(input)?;
let ret_type = parse_input_type(&src.value(), &output_type).map_err(|err| {
syn::Error::new_spanned(&src, format!("failed to parse TypeScript: {err}"))
})?;
let vars = vars.map(|v| v.1);
let (stmts, vars) = if let Some(vars) = vars {
prepare_vars(&ret_type, vars)?
} else {
Default::default()
};
let cx = Ctx { vars };
let expr_for_ast_creation = ret_type.to_code(&cx);
Ok(syn::Expr::Block(ExprBlock {
attrs: Default::default(),
label: Default::default(),
block: Block {
brace_token: Default::default(),
stmts: stmts
.into_iter()
.chain(once(syn::Stmt::Expr(expr_for_ast_creation, None)))
.collect(),
},
})
.to_token_stream())
}
#[proc_macro]
pub fn ts_template(input: TokenStream) -> TokenStream {
match ts_template_impl(input.into()) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
fn parse_position(
input: proc_macro2::TokenStream,
) -> syn::Result<(Option<&'static str>, proc_macro2::TokenStream)> {
let mut iter = input.clone().into_iter().peekable();
if let Some(proc_macro2::TokenTree::Ident(ident)) = iter.peek() {
let pos = match ident.to_string().as_str() {
"Top" => Some("Top"),
"Above" => Some("Above"),
"Within" => Some("Within"),
"Below" => Some("Below"),
"Bottom" => Some("Bottom"),
_ => None,
};
if pos.is_some() {
iter.next();
let remaining: proc_macro2::TokenStream = iter.collect();
let mut remaining_iter = remaining.into_iter();
if let Some(proc_macro2::TokenTree::Group(group)) = remaining_iter.next()
&& group.delimiter() == proc_macro2::Delimiter::Brace
{
return Ok((pos, group.stream()));
}
return Err(syn::Error::new_spanned(
input,
"expected `{` after position keyword (e.g., `ts_template!(Within { ... })`)",
));
}
}
Ok((None, input))
}
fn ts_template_impl(input: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
let (position, body) = parse_position(input)?;
let insert_pos = match position {
Some("Top") => quote! { macroforge_ts::ts_syn::InsertPos::Top },
Some("Above") => quote! { macroforge_ts::ts_syn::InsertPos::Above },
Some("Within") => quote! { macroforge_ts::ts_syn::InsertPos::Within },
Some("Below") => quote! { macroforge_ts::ts_syn::InsertPos::Below },
Some("Bottom") => quote! { macroforge_ts::ts_syn::InsertPos::Bottom },
_ => quote! { macroforge_ts::ts_syn::InsertPos::Below },
};
if position == Some("Within") {
let wrapped_body = quote! { class __MF_DUMMY__ { #body } };
let mut wrapped_str = wrapped_body.to_string();
wrapped_str = crate::template::convert_doc_attributes_to_jsdoc(&wrapped_str);
wrapped_str = crate::template::normalize_template_spacing(&wrapped_str);
wrapped_str = crate::template::collapse_template_newlines(&wrapped_str);
wrapped_str = crate::template::strip_doc_comments(&wrapped_str);
let template_code = crate::template::parse_template_str(&wrapped_str)?;
Ok(quote! {
{
let (__stmts, mut __patches, __comments, __injected_streams) = #template_code;
let __full_source =
macroforge_ts::ts_syn::emit_module_items(&__stmts, &__comments);
fn __mf_clean_fragment(fragment: &str) -> String {
let mut cleaned = String::new();
for line in fragment.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with("** ") || trimmed == "**" || trimmed == "*/" {
continue;
}
cleaned.push_str(line);
cleaned.push('\n');
}
cleaned.trim_end().to_string()
}
fn __mf_extract_body_fragment(source: &str) -> Option<String> {
let marker = "class __MF_DUMMY__";
let trimmed = source.trim();
if trimmed.is_empty() {
return None;
}
let body = if let Some(pos) = source.find(marker) {
let after = &source[pos + marker.len()..];
let after_trimmed = after.trim_start();
if after_trimmed.starts_with('{') {
let brace_offset = after.len() - after_trimmed.len();
let after_brace = &after[brace_offset + 1..];
if let Some(end) = after_brace.rfind('}') {
&after_brace[..end]
} else {
after_brace
}
} else {
after
}
} else {
source
};
let body = body.trim();
if body.is_empty() {
None
} else {
Some(__mf_clean_fragment(body))
}
}
let mut __body_fragments: Vec<String> = Vec::new();
if let Some(fragment) = __mf_extract_body_fragment(&__full_source) {
if !fragment.trim().is_empty() {
__body_fragments.push(fragment);
}
}
let __full_source_empty = __full_source.trim().is_empty();
if __full_source_empty {
for __stream in __injected_streams.iter() {
if let Some(fragment) = __mf_extract_body_fragment(__stream.source()) {
if !fragment.trim().is_empty() {
__body_fragments.push(fragment);
}
}
}
}
let __body_source = if __body_fragments.is_empty() {
__full_source.as_str().to_string()
} else {
__body_fragments.join("\n")
};
if std::env::var("MF_DEBUG_WITHIN").is_ok() {
eprintln!("[MF_DEBUG_WITHIN] full_source:\n{}", __full_source);
eprintln!("[MF_DEBUG_WITHIN] body_source:\n{}", __body_source);
for (idx, __stream) in __injected_streams.iter().enumerate() {
eprintln!(
"[MF_DEBUG_WITHIN] injected_stream[{}]:\n{}",
idx,
__stream.source()
);
if std::env::var("MF_DEBUG_WITHIN_RAW").is_ok() {
eprintln!(
"[MF_DEBUG_WITHIN] injected_stream[{}] (debug): {:?}",
idx,
__stream.source()
);
}
}
}
let __source = format!("/* @macroforge:body */{}", __body_source.trim());
let mut __stream = macroforge_ts::ts_syn::TsStream::with_insert_pos(__source, #insert_pos);
__stream.runtime_patches = __patches;
for __injected in __injected_streams {
__stream.runtime_patches.extend(__injected.runtime_patches);
}
__stream
}
})
} else {
let template_code = parse_template(body)?;
Ok(quote! {
{
let (__stmts, mut __patches, __comments, __injected_streams) = #template_code;
let __source = macroforge_ts::ts_syn::emit_module_items(&__stmts, &__comments);
let mut __stream = macroforge_ts::ts_syn::TsStream::with_insert_pos(__source, #insert_pos);
__stream.runtime_patches = __patches;
for __injected in __injected_streams {
__stream = __stream.merge(__injected);
}
__stream
}
})
}
}