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}