1use proc_macro::TokenStream;
6use proc_macro2::Span;
7use quote::quote;
8use syn::{
9 parse::{Parse, ParseStream},
10 parse_macro_input, LitStr, Token,
11};
12
13#[proc_macro]
34pub fn paint(input: TokenStream) -> TokenStream {
35 let input = parse_macro_input!(input as PaintInput);
36 let parts = match parse_template(&input.template, input.template_span) {
37 Ok(p) => p,
38 Err(e) => return e.into_compile_error().into(),
39 };
40 let exprs = parts_to_exprs(&parts);
41
42 match input.mode {
43 PrintMode::Stdout => quote! {
44 {
45 use ::std::io::Write as _;
46 #( ::std::print!("{}", #exprs); )*
47 ::std::println!();
48 }
49 },
50 PrintMode::Inline => quote! {
51 {
52 use ::std::io::Write as _;
53 #( ::std::print!("{}", #exprs); )*
54 let _ = ::std::io::stdout().flush();
55 }
56 },
57 PrintMode::Stderr => quote! {
58 {
59 #( ::std::eprint!("{}", #exprs); )*
60 ::std::eprintln!();
61 }
62 },
63 }
64 .into()
65}
66
67#[proc_macro]
79pub fn styled(input: TokenStream) -> TokenStream {
80 let lit = parse_macro_input!(input as LitStr);
81 let span = lit.span();
82 let template = lit.value();
83
84 let parts = match parse_template(&template, span) {
85 Ok(p) => p,
86 Err(e) => return e.into_compile_error().into(),
87 };
88 let exprs = parts_to_exprs(&parts);
89
90 quote! {
91 {
92 use ::std::fmt::Write as _;
93 let mut __buf = ::std::string::String::new();
94 #( ::std::write!(__buf, "{}", #exprs)
95 .expect("fmt::Write to String is infallible"); )*
96 __buf
97 }
98 }
99 .into()
100}
101
102enum PrintMode {
105 Stdout,
106 Inline,
107 Stderr,
108}
109
110struct PaintInput {
111 mode: PrintMode,
112 template: String,
113 template_span: Span,
114}
115
116impl Parse for PaintInput {
117 fn parse(input: ParseStream) -> syn::Result<Self> {
118 if input.peek(syn::Ident) && !input.peek2(Token![,]) {
119 }
122 if input.peek(syn::Ident) {
123 let ident: syn::Ident = input.parse()?;
124 let _: Token![,] = input.parse()?;
125 let lit: LitStr = input.parse()?;
126 let mode = match ident.to_string().as_str() {
127 "inline" => PrintMode::Inline,
128 "stderr" => PrintMode::Stderr,
129 other => {
130 return Err(syn::Error::new(
131 ident.span(),
132 format!("unknown paint! mode `{other}`; expected `inline` or `stderr`"),
133 ))
134 }
135 };
136 return Ok(PaintInput {
137 mode,
138 template: lit.value(),
139 template_span: lit.span(),
140 });
141 }
142 let lit: LitStr = input.parse()?;
143 Ok(PaintInput {
144 mode: PrintMode::Stdout,
145 template: lit.value(),
146 template_span: lit.span(),
147 })
148 }
149}
150
151#[derive(Debug)]
154enum Part {
155 Literal(String),
157 Styled {
159 styles: Vec<String>,
160 inner: Vec<Part>,
161 },
162 Expr(String),
164}
165
166fn parse_template(template: &str, span: Span) -> syn::Result<Vec<Part>> {
169 let chars: Vec<char> = template.chars().collect();
170 let mut parser = Parser {
171 chars,
172 pos: 0,
173 span,
174 };
175 parser.parse_sequence(false)
176}
177
178struct Parser {
179 chars: Vec<char>,
180 pos: usize,
181 span: Span,
182}
183
184impl Parser {
185 fn peek(&self) -> Option<char> {
186 self.chars.get(self.pos).copied()
187 }
188
189 fn advance(&mut self) -> Option<char> {
190 let c = self.chars.get(self.pos).copied();
191 if c.is_some() {
192 self.pos += 1;
193 }
194 c
195 }
196
197 fn parse_sequence(&mut self, stop_at_close: bool) -> syn::Result<Vec<Part>> {
199 let mut parts = Vec::new();
200 let mut literal = String::new();
201
202 loop {
203 match self.peek() {
204 None => break,
205 Some('}') if stop_at_close => {
206 self.advance();
207 break;
208 }
209 Some('{') => {
210 if !literal.is_empty() {
211 parts.push(Part::Literal(std::mem::take(&mut literal)));
212 }
213 self.advance(); parts.push(self.parse_brace()?);
215 }
216 Some(c) => {
217 literal.push(c);
218 self.advance();
219 }
220 }
221 }
222
223 if !literal.is_empty() {
224 parts.push(Part::Literal(literal));
225 }
226 Ok(parts)
227 }
228
229 fn parse_brace(&mut self) -> syn::Result<Part> {
231 let save = self.pos;
234 let first_word = self.read_word();
235
236 if !first_word.is_empty() && looks_like_style_spec(&first_word) {
237 let styles = parse_style_spec(&first_word, self.span)?;
239 if self.peek() == Some(' ') {
241 self.advance();
242 }
243 let inner = self.parse_sequence(true)?;
244 Ok(Part::Styled { styles, inner })
245 } else {
246 self.pos = save;
248 let expr = self.read_until_close_brace()?;
249 Ok(Part::Expr(expr.trim().to_string()))
250 }
251 }
252
253 fn read_word(&mut self) -> String {
255 let mut word = String::new();
256 while let Some(c) = self.peek() {
257 if c.is_whitespace() || c == '{' || c == '}' {
258 break;
259 }
260 word.push(c);
261 self.advance();
262 }
263 word
264 }
265
266 fn read_until_close_brace(&mut self) -> syn::Result<String> {
268 let mut s = String::new();
269 let mut depth = 1usize;
270 loop {
271 match self.advance() {
272 None => {
273 return Err(syn::Error::new(
274 self.span,
275 "unclosed `{` in paint! template",
276 ));
277 }
278 Some('{') => {
279 depth += 1;
280 s.push('{');
281 }
282 Some('}') => {
283 depth -= 1;
284 if depth == 0 {
285 break;
286 }
287 s.push('}');
288 }
289 Some(c) => s.push(c),
290 }
291 }
292 Ok(s)
293 }
294}
295
296fn looks_like_style_spec(spec: &str) -> bool {
300 spec.split('.').all(|t| {
301 let t = t.trim();
302 is_known_style(t) || t.starts_with("rgb(") || t.starts_with("hex(")
303 })
304}
305
306fn is_known_style(token: &str) -> bool {
307 matches!(
308 token,
309 "black"
310 | "red"
311 | "green"
312 | "yellow"
313 | "blue"
314 | "magenta"
315 | "cyan"
316 | "white"
317 | "bright_black"
318 | "bright_red"
319 | "bright_green"
320 | "bright_yellow"
321 | "bright_blue"
322 | "bright_magenta"
323 | "bright_cyan"
324 | "bright_white"
325 | "on_black"
326 | "on_red"
327 | "on_green"
328 | "on_yellow"
329 | "on_blue"
330 | "on_magenta"
331 | "on_cyan"
332 | "on_white"
333 | "on_bright_black"
334 | "on_bright_red"
335 | "on_bright_green"
336 | "on_bright_yellow"
337 | "on_bright_blue"
338 | "on_bright_magenta"
339 | "on_bright_cyan"
340 | "on_bright_white"
341 | "bold"
342 | "dim"
343 | "italic"
344 | "underline"
345 | "blink"
346 | "blink_fast"
347 | "reverse"
348 | "hidden"
349 | "strikethrough"
350 )
351}
352
353fn parse_style_spec(spec: &str, span: Span) -> syn::Result<Vec<String>> {
354 let mut result = Vec::new();
355 for token in spec.split('.') {
356 let token = token.trim();
357 if token.is_empty() {
358 continue;
359 }
360 if is_known_style(token) || token.starts_with("rgb(") || token.starts_with("hex(") {
361 result.push(token.to_string());
362 } else {
363 return Err(syn::Error::new(
364 span,
365 format!(
366 "unknown style `{token}` in paint! template\n\
367 hint: valid colors are red, green, blue, yellow, magenta, cyan, white, black\n\
368 hint: valid attributes are bold, dim, italic, underline, strikethrough, reverse"
369 ),
370 ));
371 }
372 }
373 Ok(result)
374}
375
376fn parts_to_exprs(parts: &[Part]) -> Vec<proc_macro2::TokenStream> {
379 parts.iter().map(part_to_expr).collect()
380}
381
382fn part_to_expr(part: &Part) -> proc_macro2::TokenStream {
383 match part {
384 Part::Literal(s) => {
385 quote! { #s }
386 }
387
388 Part::Expr(e) => {
389 let tokens: proc_macro2::TokenStream = match e.parse() {
390 Ok(t) => t,
391 Err(lex_err) => {
392 let msg =
393 format!("invalid expression `{e}` in paint!/styled! template: {lex_err}");
394 return quote! { { compile_error!(#msg); "" } };
395 }
396 };
397 quote! { &::std::format!("{}", #tokens) }
398 }
399
400 Part::Styled { styles, inner } => {
401 let inner_exprs = parts_to_exprs(inner);
402 let method_chain = build_method_chain(styles);
403
404 quote! {
408 {
409 use ::std::fmt::Write as _;
410 use ::spraypaint::Colorize as _;
411 let mut __inner = ::std::string::String::new();
412 #( ::std::write!(__inner, "{}", #inner_exprs)
413 .expect("fmt::Write to String is infallible"); )*
414 __inner #method_chain
415 }
416 }
417 }
418 }
419}
420
421fn build_method_chain(styles: &[String]) -> proc_macro2::TokenStream {
423 let mut chain = quote! {};
424 for style in styles {
425 if let Some(inner) = style.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
426 let parts: Vec<&str> = inner.split(',').collect();
427 if parts.len() == 3 {
428 if let (Ok(r), Ok(g), Ok(b)) = (
429 parts[0].trim().parse::<u8>(),
430 parts[1].trim().parse::<u8>(),
431 parts[2].trim().parse::<u8>(),
432 ) {
433 chain = quote! { #chain .rgb(#r, #g, #b) };
434 continue;
435 }
436 }
437 }
438 if let Some(inner) = style.strip_prefix("hex(").and_then(|s| s.strip_suffix(')')) {
439 chain = quote! { #chain .hex(#inner) };
440 continue;
441 }
442 let ident = proc_macro2::Ident::new(style, Span::call_site());
443 chain = quote! { #chain .#ident() };
444 }
445 chain
446}