colorize_proc_macro/
lib.rs

1use proc_macro::TokenStream;
2use syn::{
3    parse::{Parse, ParseStream},
4    parse_macro_input,
5    punctuated::Punctuated,
6    Error, Expr, Ident, LitStr, Result, Token,
7};
8
9mod colors;
10
11#[allow(dead_code)]
12#[derive(Debug)]
13struct WithFormatString {
14    fstring: LitStr,
15    sep: Token![,],
16    rest: TokenStream,
17}
18
19#[allow(dead_code)]
20#[derive(Debug)]
21struct ColorizeAll {
22    ident: Ident,
23    tok: Token![=>],
24    rest: TokenStream,
25}
26
27#[allow(dead_code)]
28#[derive(Debug)]
29pub(crate) struct ColorizeItem {
30    pub ident: Ident,
31    pub sep: Token![->],
32    pub msg: Expr,
33}
34
35#[derive(Debug)]
36pub(crate) enum Args {
37    Item(ColorizeItem),
38    Expr(Expr),
39}
40
41impl Parse for WithFormatString {
42    fn parse(input: ParseStream) -> Result<Self> {
43        Ok(Self {
44            fstring: input.parse()?,
45            sep: input.parse()?,
46            rest: input.parse::<proc_macro2::TokenStream>()?.into(),
47        })
48    }
49}
50
51impl Parse for ColorizeAll {
52    fn parse(input: ParseStream) -> Result<Self> {
53        Ok(Self {
54            ident: input.parse()?,
55            tok: input.parse()?,
56            rest: input.parse::<proc_macro2::TokenStream>()?.into(),
57        })
58    }
59}
60
61impl Parse for ColorizeItem {
62    fn parse(input: ParseStream) -> Result<Self> {
63        Ok(Self {
64            ident: input.parse()?,
65            sep: input.parse()?,
66            msg: input.parse()?,
67        })
68    }
69}
70
71impl Parse for Args {
72    fn parse(input: ParseStream) -> Result<Self> {
73        if input.peek(Ident) && input.peek2(Token![->]) {
74            input.parse().map(Args::Item)
75        } else {
76            input.parse().map(Args::Expr)
77        }
78    }
79}
80
81fn valid_color_all(tag: &Ident) -> Result<()> {
82    let str_tag = tag.to_string();
83    let mut it = str_tag.chars().peekable();
84
85    while let Some(t) = it.next() {
86        match t {
87            'b' | 'i' | 'u' | 'N' => continue,
88            'F' => {
89                if let Some(n) = it.peek() {
90                    match n {
91                        'k' | 'r' | 'g' | 'y' | 'b' | 'm' | 'c' | 'w' => continue,
92                        e => {
93                            return Err(Error::new(
94                                tag.span(),
95                                format!("'F{e}' Invalid foreground option - '{e}'"),
96                            ))
97                        }
98                    }
99                } else {
100                    return Err(Error::new(
101                        tag.span(),
102                        "Forground option must be followed by a valid identifier",
103                    ));
104                }
105            }
106            'B' => {
107                if let Some(n) = it.peek() {
108                    match n {
109                        'k' | 'r' | 'g' | 'y' | 'b' | 'm' | 'c' | 'w' => continue,
110                        e => {
111                            return Err(Error::new(
112                                tag.span(),
113                                format!("'B{e}' Invalid background option - '{e}'"),
114                            ))
115                        }
116                    }
117                } else {
118                    return Err(Error::new(
119                        tag.span(),
120                        "Background option must be followed by a valid identifier",
121                    ));
122                }
123            }
124            _ => {
125                return Err(Error::new(
126                    tag.span(),
127                    format!("Invalid format identifier '{t}'"),
128                ))
129            }
130        }
131    }
132    Ok(())
133}
134
135#[proc_macro]
136/// Adds ANSI color escape sequences to inputs
137///
138/// ## Usage
139///
140/// `colorize!` takes a series of inputs, with or without tokens, and converts the inputs into a `String` with ANSI escape sequences added in.
141///
142/// The returned `String` is primarily useful for printing out to a terminal which is capable of showing color.
143/// However, if all you want to do is print, and want to cut out the extra code, use `print_color` instead.
144///
145/// ## Valid inputs
146/// `colorize!` uses the same formatting style as [`format!`](std::format!) so it follows the same
147/// argument rules.
148///
149/// ```
150/// # use colorize_proc_macro as colorize;
151/// use std::path::PathBuf;
152/// use colorize::colorize;
153///
154/// let user_path = PathBuf::from("/home/color/my_new_file.txt");
155/// let pretty_path = colorize!("{:?}", Fgu->user_path.clone());
156///
157/// assert_eq!(
158///     String::from("\x1b[32;4m\"/home/color/my_new_file.txt\"\x1b[0m"),
159///     pretty_path
160/// );
161/// ```
162///
163/// ## Tokens
164/// Tokens can change color or font styling depending on their order and usage.
165///
166/// #### Styling
167/// 1. b -> bold
168/// 2. u -> underline
169/// 3. i -> italic
170///
171/// #### Color
172/// Color tokens start with an `F` (for foreground) or `B` (for background)
173///
174/// 1. Fb/Bb -> blue
175/// 2. Fr/Br -> red
176/// 3. Fg/Bg -> green
177/// 4. Fy/By -> yellow
178/// 5. Fm/By -> magenta
179/// 6. Fc/Bc -> cyan
180/// 7. Fw/Bw -> white
181/// 8. Fk/Bk -> black
182///
183/// #### Special Newline Token
184/// If you want to add a newline  within the string, include a `N` token at the start
185/// of the word(s) you wish to be on the newline. This is the same as just adding '\n' to the
186/// string, so it's up to you to use it or not.
187///
188///
189/// Example -
190/// ```
191/// # use colorize_proc_macro as colorize;
192/// use colorize::colorize;
193///
194/// let color_string = colorize!(
195///     "{} {}",
196///     b->"Hello", // First line
197///     Nb->"world, it's me!" // "world..." will be on the new line
198/// );
199///
200/// let same_color_string = colorize!(
201///    "{} \n{}",
202///    b->"Hello",
203///    b->"world, it's me!"
204/// );
205///
206/// assert_eq!(color_string, same_color_string);
207/// ```
208///
209/// #### Format Multiple Inputs
210/// You also have the ability to apply a token to multiple inputs by using `=>` at the beginning of the call.
211///
212/// ```
213/// # use colorize_proc_macro as colorize;
214/// use colorize::colorize;
215///
216/// let color_string = colorize!("{} {}", b => Fg->"Hello", By->"world");
217/// ```
218/// In the above example, "Hello" will have a green foreground, and "world" will have a yellow background. The preceeding `b =>` applies bold formatting to both.
219///
220/// ### Examples
221/// ```
222/// # use colorize_proc_macro as colorize;
223/// use colorize::colorize;
224///
225/// // Returns "Hello" in bold green
226/// let color_string = colorize!("{}", Fgb->"Hello");
227/// assert_eq!(String::from("\x1b[32;1mHello\x1b[0m"), color_string);
228///
229/// // Returns "Hello" in italic blue and "World" underlined in magenta
230/// // ", it's me" will be unformatted
231/// let color_string = colorize!("{} {} {}", iFb->"Hello", Fmu->"world", ", it's me!");
232/// assert_eq!(String::from("\x1b[3;34mHello\x1b[0m \x1b[35;4mworld\x1b[0m , it's me!"), color_string);
233/// ```
234pub fn colorize(input: TokenStream) -> TokenStream {
235    let inp = input.clone();
236
237    let (fstring, inp) = match syn::parse::<WithFormatString>(inp) {
238        Ok(r) => (r.fstring, r.rest),
239        Err(e) => return e.into_compile_error().into(),
240    };
241
242    let (args, id) = match syn::parse::<ColorizeAll>(inp.clone()) {
243        Ok(r) => {
244            if let Err(e) = valid_color_all(&r.ident) {
245                e.into_compile_error();
246            }
247            let rem = r.rest;
248            let a = parse_macro_input!(rem with Punctuated::<Args, Token![,]>::parse_terminated);
249
250            (a, Some(r.ident))
251        }
252        Err(_) => {
253            let a = parse_macro_input!(inp with Punctuated::<Args, Token![,]>::parse_terminated);
254            (a, None)
255        }
256    };
257
258    let res = match crate::colors::parse_fstring(&fstring, args, id) {
259        Ok(r) => r,
260        Err(e) => return e.into_compile_error().into(),
261    };
262
263    res.into()
264}