webrust_macros/
lib.rs

1// webrust/webrust-macros/src/lib.rs
2//! # webrust-macros — Proc-macros for WebRust
3//!
4//! Provides the **`#[gui(...)]`** attribute and rewrites of `print`/`println`
5//! to support:
6//! - **F-strings**-like formatting: `println("Hello {user:?}, score={score:.2}")`
7//! - **Inline LaTeX** with `$( ... )` (preserved for the renderer)
8//! - **`#[gui(Font, Size, color, !bg)]`**: boots the GUI server and applies theme
9//!
10//! ## `#[gui(...)]` Attribute
11//! Compact, order-agnostic syntax:
12//! - Font (capitalized tokens): `Arial`, `Times_New_Roman` → spaces restored
13//! - Size: `14px`, `16px`, …
14//! - Text color: `black`, `#222`, `crimson`, …
15//! - Background: `!white`, `!#f8fafc`, …
16//!
17//! Example:
18//! ```rust,no_run
19//! use webrust::prelude::*;
20//!
21//! #[gui(Times_New_Roman 13px white !fuchsia)]
22//! fn main() {
23//!     let user = "Ada";
24//!     let score = 42.195;
25//!     println("Hi {user:?}, score={score:.2}");
26//!     println("Vector json: { [1,2,3] :j }");
27//! }
28//! ```
29//!
30//! ## Specifiers inside `{ ... }`
31//! - `{:?}`   → Debug
32//! - `{:j}`   → Pretty JSON (HTML-safe, monospace block)
33//! - `{:c}`   → Compact debug (stringified)
34//! - `{:...}` → Any standard `format!` spec (e.g., `:.2`, `:>6`, …)
35//!
36//! ## Integration
37//! - `#[gui]` wraps your function: initializes theme, starts **io::gui**, runs your code.
38//! - Calls to `print`/`println` are rewritten into optimized `format!` pipelines.
39//!
40//! ## Performance
41//! - Fast scanning with `memchr`/`memmem`, minimized allocations
42//! - Pre-sized buffers and compact tokenization
43//!
44//! See `webrust::io::gui` for the runtime (HTTP API, validation, shutdown).
45
46use memchr::{memchr, memchr2, memmem};
47use proc_macro::TokenStream;
48use quote::{quote, ToTokens};
49use smallvec::SmallVec;
50use std::borrow::Cow;
51use syn::{
52    parse_macro_input,
53    visit_mut::{visit_expr_mut, VisitMut},
54    Expr, ExprCall, ExprLit, ExprPath, ItemFn, Lit,
55};
56
57type TS = proc_macro2::TokenStream;
58
59#[inline(always)]
60fn has_fstring_or_latex(b: &[u8]) -> bool {
61    memchr(b'{', b).is_some() || memmem::find(b, b"$(").is_some()
62}
63
64#[inline]
65fn cut(s: &str) -> (&str, Option<&str>) {
66    let b = s.as_bytes();
67    let (n, mut i, mut p, mut a, mut br) = (b.len(), 0, 0, 0, 0);
68    while i < n {
69        match b[i] {
70            b'(' => p += 1,
71            b')' => p -= 1,
72            b'<' => a += 1,
73            b'>' => a -= 1,
74            b'[' => br += 1,
75            b']' => br -= 1,
76            b':' if p == 0 && a == 0 && br == 0 => {
77                if i + 1 < n && b[i + 1] == b':' {
78                    i += 2;
79                    continue;
80                }
81                let (e, x) = s.split_at(i);
82                return (e.trim(), Some(x[1..].trim()));
83            }
84            _ => {}
85        }
86        i += 1;
87    }
88    (s.trim(), None)
89}
90
91#[inline]
92fn latex_ranges(t: &str) -> SmallVec<[(usize, usize); 8]> {
93    let b = t.as_bytes();
94    let mut v = SmallVec::new();
95    let mut i = 0;
96    while let Some(pos) = memmem::find(&b[i..], b"$(") {
97        let s = i + pos;
98        let mut d = 1;
99        let mut j = s + 2;
100        while j < b.len() && d > 0 {
101            match b[j] {
102                b'(' => d += 1,
103                b')' => d -= 1,
104                _ => {}
105            }
106            j += 1;
107        }
108        v.push((s, j));
109        i = j;
110    }
111    v
112}
113
114#[inline]
115fn escape_braces_cow(s: &str) -> Cow<'_, str> {
116    if memchr2(b'{', b'}', s.as_bytes()).is_none() {
117        return Cow::Borrowed(s);
118    }
119    let mut o = String::with_capacity(s.len() + (s.len() >> 3) + 8);
120    for ch in s.chars() {
121        match ch {
122            '{' => o.push_str("{{"),
123            '}' => o.push_str("}}"),
124            _ => o.push(ch),
125        }
126    }
127    Cow::Owned(o)
128}
129
130#[inline]
131fn find_matching_brace(b: &[u8], mut pos: usize) -> Option<usize> {
132    let mut depth = 1;
133    while depth > 0 {
134        let off = memchr2(b'{', b'}', &b[pos..])?;
135        pos += off;
136        match b[pos] {
137            b'{' => depth += 1,
138            b'}' => depth -= 1,
139            _ => {}
140        }
141        pos += 1;
142    }
143    Some(pos)
144}
145
146fn trans(t: &str) -> (String, SmallVec<[TS; 8]>) {
147    let b = t.as_bytes();
148    if !has_fstring_or_latex(b) {
149        return (t.to_string(), SmallVec::new());
150    }
151    let n = b.len();
152    let rs = latex_ranges(t);
153    let mut fmt = String::with_capacity(n + (rs.len() << 4));
154    let mut args: SmallVec<[TS; 8]> = SmallVec::new();
155    let (mut r, mut i, mut last) = (0, 0, 0);
156    while i < n {
157        if r < rs.len() && i == rs[r].0 {
158            fmt.push_str(escape_braces_cow(&t[last..rs[r].1]).as_ref());
159            i = rs[r].1;
160            last = i;
161            r += 1;
162            continue;
163        }
164        match b[i] {
165            b'{' => {
166                if i + 1 < n && b[i + 1] == b'{' {
167                    fmt.push_str(&t[last..i + 2]);
168                    i += 2;
169                    last = i;
170                    continue;
171                }
172                fmt.push_str(&t[last..i]);
173                i += 1;
174                let s = i;
175                let end = match find_matching_brace(b, i) {
176                    Some(p) => p,
177                    None => {
178                        fmt.push_str("{:?}");
179                        break;
180                    }
181                };
182                i = end;
183                let e = i - 1;
184                let inner = t[s..e].trim();
185                if inner.is_empty() {
186                    fmt.push_str("{:?}");
187                    last = i;
188                    continue;
189                }
190                let (ex, sp) = cut(inner);
191                if let Ok(expr) = syn::parse_str::<Expr>(ex) {
192                    match sp {
193                        Some("?") => {
194                            fmt.push_str("{:?}");
195                            args.push(expr.into_token_stream());
196                        }
197                        Some("c") => {
198                            fmt.push_str("{}");
199                            args.push(quote! { format!("{:?}", #expr) });
200                            last = i;
201                            continue;
202                        }
203                        Some("j") => {
204                            fmt.push_str("{}");
205                            args.push(quote! { __w_json(&#expr) });
206                            last = i;
207                            continue;
208                        }
209                        Some(sp) => {
210                            fmt.push('{');
211                            fmt.push(':');
212                            fmt.push_str(sp);
213                            fmt.push('}');
214                            args.push(expr.into_token_stream());
215                        }
216                        None => {
217                            fmt.push_str("{}");
218                            args.push(expr.into_token_stream());
219                        }
220                    }
221                } else {
222                    fmt.push('{');
223                    fmt.push_str(inner);
224                    fmt.push('}');
225                }
226                last = i;
227            }
228            b'}' => {
229                if i + 1 < n && b[i + 1] == b'}' {
230                    fmt.push_str(&t[last..i + 2]);
231                    i += 2;
232                    last = i;
233                } else {
234                    fmt.push_str(&t[last..=i]);
235                    i += 1;
236                    last = i;
237                }
238            }
239            _ => i += 1,
240        }
241    }
242    if last < n {
243        fmt.push_str(&t[last..]);
244    }
245    (fmt, args)
246}
247
248struct R;
249impl VisitMut for R {
250    fn visit_expr_mut(&mut self, e: &mut Expr) {
251        if let Expr::Call(ExprCall { func, args, .. }) = e {
252            if let Expr::Path(ExprPath { path, .. }) = func.as_ref() {
253                if path.segments.len() == 1 {
254                    let id = &path.segments[0].ident;
255                    let id_str = id.to_string();
256                    if id_str == "println" || id_str == "print" {
257                        if let Some(Expr::Lit(ExprLit {
258                            lit: Lit::Str(s), ..
259                        })) = args.first()
260                        {
261                            let (f, a) = trans(&s.value());
262                            let lit = syn::LitStr::new(&f, s.span());
263                            *e = syn::parse2(quote! { #id(format!(#lit #(, #a)*)) }).unwrap();
264                            return;
265                        }
266                    }
267                }
268            }
269        }
270        visit_expr_mut(self, e);
271    }
272}
273
274#[inline]
275fn parse_gui_args(ts: TokenStream) -> (String, String, String, String) {
276    let s = ts.to_string();
277    if s.is_empty() {
278        return (
279            "Arial".into(),
280            "14px".into(),
281            "black".into(),
282            "white".into(),
283        );
284    }
285    let (mut size, mut color, mut bg) = ("14px".into(), "black".into(), "white".into());
286    let mut font_parts = Vec::with_capacity(4);
287    for tok in s.split_whitespace() {
288        let tok = tok.trim_matches(|c| c == ',' || c == '"');
289        if tok.is_empty() {
290            continue;
291        }
292        if let Some(pos) = tok.find('!') {
293            if pos > 0 {
294                color = tok[..pos].into();
295            }
296            bg = tok[pos + 1..].into();
297        } else if tok.starts_with('!') {
298            bg = tok[1..].into();
299        } else if tok.ends_with("px") || tok.as_bytes()[0].is_ascii_digit() {
300            size = tok.into();
301        } else if tok.as_bytes()[0].is_ascii_uppercase() {
302            font_parts.push(tok);
303        } else {
304            color = tok.into();
305        }
306    }
307    let font = if !font_parts.is_empty() {
308        font_parts.join(" ")
309    } else {
310        "Arial".into()
311    };
312    (font, size, color, bg)
313}
314
315#[proc_macro_attribute]
316pub fn gui(attr: TokenStream, input: TokenStream) -> TokenStream {
317    let mut f = parse_macro_input!(input as ItemFn);
318    R.visit_item_fn_mut(&mut f);
319    let (font, size, color, bg) = parse_gui_args(attr);
320    let body = &f.block;
321    let wrapped = quote! {{
322        webrust::io::print::set_defaults(#color.to_string(), #font.to_string(), #size.to_string());
323        fn __w_json<T: webrust::serde::Serialize>(v: &T) -> String {
324            use webrust::serde_json::Value;
325            fn write_number(n: &webrust::serde_json::Number, out: &mut String) {
326                if let Some(i) = n.as_i64() { let mut b = webrust::itoa::Buffer::new(); out.push_str(b.format(i)); }
327                else if let Some(u) = n.as_u64() { let mut b = webrust::itoa::Buffer::new(); out.push_str(b.format(u)); }
328                else if let Some(f) = n.as_f64() { let mut b = webrust::ryu::Buffer::new(); out.push_str(b.format(f)); }
329                else { out.push_str(&n.to_string()); }
330            }
331            fn fmt_into(val: &Value, depth: usize, out: &mut String) {
332                match val {
333                    Value::Array(arr) => {
334                        if arr.is_empty() { out.push_str("[]"); return; }
335                        if arr.len() <= 3 && arr.iter().all(|x| x.is_number()) {
336                            out.push('[');
337                            for (i, x) in arr.iter().enumerate() {
338                                if i > 0 { out.push(' '); }
339                                if let Value::Number(n) = x { write_number(n, out) } else { fmt_into(x, depth, out) }
340                            }
341                            out.push(']'); return;
342                        }
343                        let ind = "    ".repeat(depth);
344                        let inn = "    ".repeat(depth + 1);
345                        out.push('['); out.push('\n');
346                        for x in arr.iter() { out.push_str(&inn); fmt_into(x, depth + 1, out); out.push('\n'); }
347                        out.push_str(&ind); out.push(']');
348                    }
349                    Value::Object(obj) => {
350                        if obj.is_empty() { out.push_str("{}"); return; }
351                        let mut kv: Vec<_> = obj.iter().collect();
352                        kv.sort_unstable_by(|a, b| a.0.cmp(b.0));
353                        let ind = "    ".repeat(depth);
354                        let inn = "    ".repeat(depth + 1);
355                        out.push('{'); out.push('\n');
356                        for (k, v) in kv { out.push_str(&inn); out.push('"'); out.push_str(k); out.push_str(r#"": "#); fmt_into(v, depth + 1, out); out.push('\n'); }
357                        out.push_str(&ind); out.push('}');
358                    }
359                    Value::String(s) => { out.push('"'); out.push_str(s); out.push('"'); }
360                    Value::Number(n) => write_number(n, out),
361                    Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
362                    Value::Null => out.push_str("null"),
363                }
364            }
365            let val = webrust::serde_json::to_value(v).unwrap_or(Value::Null);
366            let mut raw = String::with_capacity(128);
367            fmt_into(&val, 0, &mut raw);
368            let mut escaped = String::with_capacity(raw.len() + (raw.len() >> 3) + 32);
369            for ch in raw.chars() {
370                match ch { '&' => escaped.push_str("&amp;"), '<' => escaped.push_str("&lt;"), '>' => escaped.push_str("&gt;"), ' ' => escaped.push_str("&nbsp;"), _ => escaped.push(ch) }
371            }
372            format!(r#"<div style="font-family:'Courier New',monospace;color:#1e40af;font-size:12px;line-height:1.3;white-space:pre;">{}</div>"#, escaped)
373        }
374        let style = webrust::io::gui::StyleConfig { bg: #bg.into(), color: #color.into(), font: #font.into(), size: #size.into() };
375        webrust::io::gui::start_gui_server_with_style(style, || { #body });
376    }};
377    f.block = syn::parse2(wrapped).unwrap();
378    TokenStream::from(quote! { #[allow(unused_variables, dead_code)] #f })
379}