1use 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("&"), '<' => escaped.push_str("<"), '>' => escaped.push_str(">"), ' ' => escaped.push_str(" "), _ => 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}