color_print_proc_macro/format_args/
mod.rs

1//! Parses the `format!`-like arguments and the format string.
2
3mod format_arg;
4
5use proc_macro::TokenStream;
6use syn::spanned::Spanned;
7use syn::{
8    self, parse::Parser, punctuated::Punctuated, token::Comma, Expr, ExprLit, Lit, LitStr, Token,
9};
10
11use crate::color_context::ColorTag;
12use crate::error::{Error, SpanError};
13use crate::parse;
14use crate::util::{self, inner_span};
15use format_arg::FormatArg;
16
17/// Retrieves the original format string and arguments given to the public macros.
18pub fn get_args_and_format_string(
19    input: TokenStream,
20) -> Result<(LitStr, Punctuated<FormatArg, Comma>), SpanError> {
21    let args = parse_args(input)?;
22    let format_string = get_format_string(args.first())?;
23    Ok((format_string, args))
24}
25
26/// Parses the arguments of a `format!`-like macro.
27pub fn parse_args(input: TokenStream) -> Result<Punctuated<FormatArg, Comma>, SpanError> {
28    let parser = Punctuated::<FormatArg, Token![,]>::parse_terminated;
29    parser
30        .parse(input)
31        .map_err(|e| Error::Parse(e.to_string()).into())
32}
33
34/// Gets the format string.
35pub fn get_format_string(arg: Option<&FormatArg>) -> Result<LitStr, SpanError> {
36    match arg {
37        Some(FormatArg {
38            expr: Expr::Lit(ExprLit {
39                lit: Lit::Str(s), ..
40            }),
41            ..
42        }) => Ok(s.to_owned()),
43        Some(bad_arg) => {
44            Err(SpanError::new(Error::MustBeStringLiteral, Some(bad_arg.span()),))
45        }
46        None => Ok(util::literal_string(""))
47    }
48}
49
50/// A node inside a format string. The two variants `Text` and `Placeholder` represent the usual
51/// kind of nodes that can be found in `format!`-like macros.
52///
53/// E.g., the format string `"Hello, {:?}, happy day"` will have 3 nodes:
54///  - `Text("Hello, ")`,
55///  - `Placeholder("{:?}")`,
56///  - `Text(", happy day")`.
57///
58/// The third kind of node: `Color(&str)`, represents a color code to apply.
59///
60/// E.g., the format string `"This is a <blue>{}<clear> idea"` will have 7 nodes:
61///  - `Text("This is a ")`
62///  - `Color("blue")`
63///  - `Placeholder("{}")`
64///  - `Color("clear")`
65///  - `Text(" idea")`
66#[derive(Debug)]
67pub enum Node<'a> {
68    Text(&'a str),
69    Placeholder(&'a str),
70    ColorTagGroup(Vec<ColorTag<'a>>),
71}
72
73/// Parses a format string which may contain usual format placeholders (`{...}`) as well as color
74/// codes like `"<red>"`, `"<blue,bold>"`.
75pub fn parse_format_string<'a>(
76    input: &'a str,
77    lit_str: &LitStr,
78) -> Result<Vec<Node<'a>>, SpanError> {
79    /// Representation of the parsing context. Each variant's argument is the start offset of the
80    /// given parse context.
81    enum Context {
82        /// The char actually parsed is a textual character:
83        Text(usize),
84        /// The char actually parsed is part of a `format!`-like placeholder:
85        Placeholder(usize),
86        /// The char actually parsed is part of a color tag, like `<red>`:
87        Color(usize),
88    }
89
90    macro_rules! span {
91        ($inside:expr) => { inner_span(input, lit_str, $inside) };
92    }
93    macro_rules! err {
94        ([$inside:expr] $($e:tt)*) => { SpanError::new($($e)*, Some(span!($inside))) };
95        ($($e:tt)*) => { SpanError::new($($e)*, Some(lit_str.span())) };
96    }
97
98    let mut context = Context::Text(0);
99    let mut nodes = vec![];
100    let mut close_angle_bracket_idx: Option<usize> = None;
101    let mut nb_open_tags: isize = 0;
102
103    for (i, c) in input.char_indices() {
104        match context {
105            Context::Text(text_start) => {
106                let mut push_text = false;
107                if c == '{' {
108                    // The start of a placeholder:
109                    context = Context::Placeholder(i);
110                    push_text = true;
111                } else if c == '<' {
112                    // The start of a color code:
113                    context = Context::Color(i);
114                    push_text = true;
115                } else if c == '>' {
116                    // Double close angle brackets ">>":
117                    if let Some(idx) = close_angle_bracket_idx {
118                        if i == idx + 1 {
119                            context = Context::Text(i + 1);
120                            push_text = true;
121                        }
122                        close_angle_bracket_idx = None;
123                    } else {
124                        close_angle_bracket_idx = Some(i);
125                    }
126                };
127                if push_text && text_start != i {
128                    nodes.push(Node::Text(&input[text_start..i]));
129                }
130            }
131            Context::Placeholder(ph_start) => {
132                if c == '{' && i == ph_start + 1 {
133                    // Double curly brackets "{{":
134                    context = Context::Text(ph_start);
135                } else if c == '}' {
136                    // The end of a placeholder:
137                    nodes.push(Node::Placeholder(&input[ph_start..i + 1]));
138                    context = Context::Text(i + 1);
139                }
140            }
141            Context::Color(tag_start) => {
142                if c == '<' && i == tag_start + 1 {
143                    // Double open angle brackets "<<":
144                    context = Context::Text(tag_start + 1);
145                } else if c == '>' {
146                    // The end of a color code:
147                    let tag_input = &input[tag_start..i + 1];
148                    let mut tag = parse::color_tag(tag_input)
149                        .map_err(|e| {
150                            use nom::Err;
151                            let (input, error) = match e {
152                                Err::Error(parse::Error { detail: Some(d), .. }) |
153                                Err::Failure(parse::Error { detail: Some(d), .. }) => {
154                                    (d.input, Error::ParseTag(d.message))
155                                }
156                                // Should never happen:
157                                _ => (tag_input, Error::UnableToParseTag(tag_input.to_string())),
158                            };
159                            err!([input] error)
160                        })?
161                        .1;
162                    tag.set_span(span!(tag_input));
163                    nb_open_tags += if tag.is_close { -1 } else { 1 };
164                    // Group consecutive tags into one group, in order to improve optimization
165                    // (e.g., "<blue><green>" will be optimized by removing the useless "<blue>"
166                    // ANSI sequence):
167                    if let Some(Node::ColorTagGroup(last_tag_group)) = nodes.last_mut() {
168                        last_tag_group.push(tag);
169                    } else {
170                        nodes.push(Node::ColorTagGroup(vec![tag]));
171                    }
172                    context = Context::Text(i + 1);
173                }
174            }
175        }
176    }
177
178    // Process the end of the string:
179    match context {
180        Context::Text(text_start) => {
181            if text_start != input.len() {
182                nodes.push(Node::Text(&input[text_start..]));
183            }
184
185            // Auto-close remaining open tags:
186            if nb_open_tags > 0 {
187                let tags = (0..nb_open_tags)
188                    .map(|_| ColorTag::new_close())
189                    .collect::<Vec<_>>();
190                nodes.push(Node::ColorTagGroup(tags));
191            }
192
193            Ok(nodes)
194        }
195        Context::Placeholder(start) => Err(err!([&input[start..]] Error::UnclosedPlaceholder)),
196        Context::Color(start) => Err(err!([&input[start..]] Error::UnclosedTag)),
197    }
198}