Skip to main content

zyn_core/
debug.rs

1//! Debug formatting and printing for template expansions.
2//!
3//! Used by the `#[zyn::debug]` macro to print expanded token streams or the
4//! template AST to stderr during development. Not intended for production use.
5//!
6//! # Example
7//!
8//! ```ignore
9//! // Replace zyn! with zyn::debug! to print the expansion to stderr:
10//! zyn::debug! { fn {{ name | snake }}() {} }
11//! // stderr: fn hello_world() {}
12//! ```
13
14use proc_macro2::TokenStream;
15
16use crate::ast;
17
18/// Returns the raw expansion code with internal names cleaned up.
19pub fn raw(tokens: &TokenStream) -> String {
20    let raw = tokens.to_string();
21    let cleaned = raw
22        .replace(
23            ":: zyn :: proc_macro2 :: TokenStream :: new ()",
24            "TokenStream::new()",
25        )
26        .replace(":: zyn :: proc_macro2 :: TokenStream", "TokenStream")
27        .replace(":: zyn :: quote :: quote !", "quote!")
28        .replace(
29            ":: zyn :: quote :: ToTokens :: to_tokens",
30            "ToTokens::to_tokens",
31        )
32        .replace(":: zyn :: Pipe :: pipe", "Pipe::pipe")
33        .replace(":: zyn :: Render :: render", "Render::render")
34        .replace(":: core :: compile_error !", "compile_error!")
35        .replace(":: core :: result :: Result :: Ok", "Ok")
36        .replace(":: core :: result :: Result :: Err", "Err")
37        .replace("__zyn_ts_0", "output")
38        .replace("__zyn_ts_1", "inner_1")
39        .replace("__zyn_ts_2", "inner_2")
40        .replace("__zyn_ts_3", "inner_3")
41        .replace("__zyn_ts_4", "inner_4")
42        .replace("__zyn_val", "value")
43        .replace("__zyn_rendered", "rendered")
44        .replace("__zyn_expand_result", "result");
45
46    fmt(&cleaned)
47}
48
49/// Returns a human-readable representation of the template AST.
50pub fn ast(element: &crate::template::Template) -> String {
51    let mut lines = Vec::new();
52    lines.push("Template [".to_string());
53
54    for node in &element.nodes {
55        let desc = match node {
56            ast::Node::Tokens(t) => format!("  Tokens({:?})", t.stream.to_string()),
57            ast::Node::Interp(_) => "  Interp { ... }".to_string(),
58            ast::Node::At(at) => {
59                let kind = if at.is_if() {
60                    "If"
61                } else if at.is_for() {
62                    "For"
63                } else if at.is_match() {
64                    "Match"
65                } else if at.is_element() {
66                    "Element"
67                } else {
68                    "Unknown"
69                };
70                format!("  At({})", kind)
71            }
72            ast::Node::Group(_) => "  Group { ... }".to_string(),
73        };
74        lines.push(desc);
75    }
76
77    lines.push("]".to_string());
78    lines.join("\n")
79}
80
81/// Pretty-prints the generated token stream to stderr.
82pub fn print(tokens: &TokenStream) {
83    let formatted = fmt(&tokens.to_string());
84    eprintln!("zyn::debug! ─── pretty\n{}", formatted);
85}
86
87/// Formats a token string with indentation and line breaks.
88pub fn fmt(input: &str) -> String {
89    let mut result = String::new();
90    let mut depth: usize = 0;
91    let mut chars = input.chars().peekable();
92
93    while let Some(ch) = chars.next() {
94        match ch {
95            '{' => {
96                result.push('{');
97                result.push('\n');
98                depth += 1;
99                push_indent(&mut result, depth);
100            }
101            '}' => {
102                depth = depth.saturating_sub(1);
103                if result.ends_with("    ") || result.ends_with('\n') {
104                    result.truncate(result.trim_end().len());
105                    result.push('\n');
106                }
107                push_indent(&mut result, depth);
108                result.push('}');
109
110                if chars
111                    .peek()
112                    .is_some_and(|c| *c != ',' && *c != ';' && *c != ')')
113                {
114                    result.push('\n');
115                    push_indent(&mut result, depth);
116                }
117            }
118            ';' => {
119                result.push(';');
120                result.push('\n');
121                push_indent(&mut result, depth);
122            }
123            ' ' if result.ends_with('\n') || result.ends_with("    ") => {}
124            _ => {
125                result.push(ch);
126            }
127        }
128    }
129
130    result.trim().to_string()
131}
132
133fn push_indent(s: &mut String, depth: usize) {
134    for _ in 0..depth {
135        s.push_str("    ");
136    }
137}