aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-console-macros/src/codegen.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-console-macros/src/codegen.rs')
-rw-r--r--crates/mozart-console-macros/src/codegen.rs155
1 files changed, 155 insertions, 0 deletions
diff --git a/crates/mozart-console-macros/src/codegen.rs b/crates/mozart-console-macros/src/codegen.rs
new file mode 100644
index 0000000..11e37f9
--- /dev/null
+++ b/crates/mozart-console-macros/src/codegen.rs
@@ -0,0 +1,155 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::Expr;
+use syn::punctuated::Punctuated;
+
+use crate::parser::Segment;
+
+/// Returns true if the string contains any format placeholders (`{}`, `{name}`, `{0}`, `{:<10}`, etc.)
+/// but not escaped braces `{{` or `}}`.
+fn has_placeholders(s: &str) -> bool {
+ let mut chars = s.chars().peekable();
+ while let Some(ch) = chars.next() {
+ if ch == '{' {
+ match chars.peek() {
+ Some('{') => {
+ chars.next(); // skip escaped
+ }
+ _ => return true,
+ }
+ } else if ch == '}' && chars.peek() == Some(&'}') {
+ chars.next(); // skip escaped
+ }
+ }
+ false
+}
+
+/// Count implicit positional placeholders (`{}` and `{:spec}`) in a format string.
+/// Named (`{name}`) and numbered (`{0}`) placeholders are NOT counted
+/// since they don't consume positional arguments.
+fn count_positional_placeholders(s: &str) -> usize {
+ let mut count = 0;
+ let mut chars = s.chars().peekable();
+ while let Some(ch) = chars.next() {
+ if ch == '{' {
+ match chars.peek() {
+ Some('{') => {
+ chars.next(); // escaped
+ }
+ Some('}') => {
+ // `{}` — implicit positional
+ count += 1;
+ chars.next();
+ }
+ Some(':') => {
+ // `{:spec}` — implicit positional with format spec
+ count += 1;
+ for c in chars.by_ref() {
+ if c == '}' {
+ break;
+ }
+ }
+ }
+ Some(c) if c.is_ascii_digit() => {
+ // `{0}`, `{0:spec}` — explicit positional, skip
+ for c in chars.by_ref() {
+ if c == '}' {
+ break;
+ }
+ }
+ }
+ _ => {
+ // `{name}` or `{name:spec}` — named, skip
+ for c in chars.by_ref() {
+ if c == '}' {
+ break;
+ }
+ }
+ }
+ }
+ } else if ch == '}' && chars.peek() == Some(&'}') {
+ chars.next();
+ }
+ }
+ count
+}
+
+pub fn generate(
+ segments: &[Segment],
+ extra_args: &Punctuated<Expr, syn::Token![,]>,
+) -> TokenStream {
+ // Single segment: pass all extra args
+ if segments.len() == 1 {
+ return generate_single(&segments[0], extra_args);
+ }
+
+ // Multiple segments: distribute positional args across segments
+ let mut pos = 0usize;
+ let mut seg_bindings = Vec::new();
+ let mut seg_idents = Vec::new();
+
+ for (i, segment) in segments.iter().enumerate() {
+ let content = segment_content(segment);
+ let n = count_positional_placeholders(content);
+ let end = (pos + n).min(extra_args.len());
+ let slice: Punctuated<Expr, syn::Token![,]> = extra_args
+ .iter()
+ .skip(pos)
+ .take(end - pos)
+ .cloned()
+ .collect();
+ pos = end;
+
+ let ident = quote::format_ident!("__seg{}", i);
+ let expr = generate_single(segment, &slice);
+ seg_bindings.push(quote! { let #ident = #expr; });
+ seg_idents.push(ident);
+ }
+
+ // Build a format string with one `{}` per segment
+ let fmt_str = seg_idents.iter().map(|_| "{}").collect::<Vec<_>>().join("");
+
+ quote! {
+ {
+ #(#seg_bindings)*
+ ::std::format!(#fmt_str, #(#seg_idents),*)
+ }
+ }
+}
+
+fn segment_content(segment: &Segment) -> &str {
+ match segment {
+ Segment::Plain(s) => s,
+ Segment::Tagged { content, .. } => content,
+ }
+}
+
+fn generate_single(segment: &Segment, args: &Punctuated<Expr, syn::Token![,]>) -> TokenStream {
+ match segment {
+ Segment::Plain(text) => {
+ if has_placeholders(text) {
+ let lit = proc_macro2::Literal::string(text);
+ quote! { ::std::format!(#lit, #args) }
+ } else {
+ quote! { ::std::string::String::from(#text) }
+ }
+ }
+ Segment::Tagged { tag, content } => {
+ let func = quote::format_ident!("{}", tag);
+ if has_placeholders(content) {
+ let lit = proc_macro2::Literal::string(content);
+ quote! {
+ ::std::string::ToString::to_string(
+ &::mozart_core::console::#func(&::std::format!(#lit, #args))
+ )
+ }
+ } else {
+ quote! {
+ ::std::string::ToString::to_string(
+ &::mozart_core::console::#func(#content)
+ )
+ }
+ }
+ }
+ }
+}