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 three public macros.
18pub 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.
27pub 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.
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: &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)]
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 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