1 | //! Parses the `format!`-like arguments and the format string. |
2 | |
3 | mod format_arg; |
4 | |
5 | use proc_macro::TokenStream; |
6 | use syn::spanned::Spanned; |
7 | use syn::{ |
8 | self, parse::Parser, punctuated::Punctuated, token::Comma, Expr, ExprLit, Lit, LitStr, Token, |
9 | }; |
10 | |
11 | use crate::color_context::ColorTag; |
12 | use crate::error::{Error, SpanError}; |
13 | use crate::parse; |
14 | use crate::util::{self, inner_span}; |
15 | use format_arg::FormatArg; |
16 | |
17 | /// Retrieves the original format string and arguments given to the three public macros. |
18 | pub fn get_args_and_format_string( |
19 | input: TokenStream, |
20 | ) -> Result<(LitStr, Punctuated<FormatArg, Comma>), SpanError> { |
21 | let args: Punctuated = parse_args(input)?; |
22 | let format_string: LitStr = get_format_string(arg:args.first())?; |
23 | Ok((format_string, args)) |
24 | } |
25 | |
26 | /// Parses the arguments of a `format!`-like macro. |
27 | pub fn parse_args(input: TokenStream) -> Result<Punctuated<FormatArg, Comma>, SpanError> { |
28 | let parser: fn parse_terminated(…) -> … = Punctuated::<FormatArg, Token![,]>::parse_terminated; |
29 | parser |
30 | .parse(input) |
31 | .map_err(|e: Error| Error::Parse(e.to_string()).into()) |
32 | } |
33 | |
34 | /// Gets the format string. |
35 | pub 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: &LitStr), .. |
40 | }), |
41 | .. |
42 | }) => Ok(s.to_owned()), |
43 | Some(bad_arg: &FormatArg) => { |
44 | Err(SpanError::new(err:Error::MustBeStringLiteral, span: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)] |
67 | pub 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>"`. |
75 | pub 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 crate::parse::nom_prelude::complete::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 | } |
199 | |