1use proc_macro2::{Span, TokenStream};
2use quote::{quote, quote_spanned, ToTokens, TokenStreamExt};
3use std::collections::HashMap;
4use syn::{
5 parse::{Parse, ParseStream},
6 *,
7};
8
9#[derive(Debug, PartialEq, Eq, Clone, Hash)]
15pub struct IfmtInput {
16 pub source: LitStr,
17 pub segments: Vec<Segment>,
18}
19
20impl IfmtInput {
21 pub fn new(span: Span) -> Self {
22 Self {
23 source: LitStr::new("", span),
24 segments: Vec::new(),
25 }
26 }
27
28 pub fn new_litstr(source: LitStr) -> Result<Self> {
29 let segments = IfmtInput::from_raw(&source.value())?;
30 Ok(Self { segments, source })
31 }
32
33 pub fn span(&self) -> Span {
34 self.source.span()
35 }
36
37 pub fn push_raw_str(&mut self, other: String) {
38 self.segments.push(Segment::Literal(other.to_string()))
39 }
40
41 pub fn push_ifmt(&mut self, other: IfmtInput) {
42 self.segments.extend(other.segments);
43 }
44
45 pub fn push_expr(&mut self, expr: Expr) {
46 self.segments.push(Segment::Formatted(FormattedSegment {
47 format_args: String::new(),
48 segment: FormattedSegmentType::Expr(Box::new(expr)),
49 }));
50 }
51
52 pub fn is_static(&self) -> bool {
53 self.segments
54 .iter()
55 .all(|seg| matches!(seg, Segment::Literal(_)))
56 }
57
58 pub fn to_static(&self) -> Option<String> {
59 self.segments
60 .iter()
61 .try_fold(String::new(), |acc, segment| {
62 if let Segment::Literal(seg) = segment {
63 Some(acc + seg)
64 } else {
65 None
66 }
67 })
68 }
69
70 pub fn dynamic_segments(&self) -> Vec<&FormattedSegment> {
71 self.segments
72 .iter()
73 .filter_map(|seg| match seg {
74 Segment::Formatted(seg) => Some(seg),
75 _ => None,
76 })
77 .collect::<Vec<_>>()
78 }
79
80 pub fn dynamic_seg_frequency_map(&self) -> HashMap<&FormattedSegment, usize> {
81 let mut map = HashMap::new();
82 for seg in self.dynamic_segments() {
83 *map.entry(seg).or_insert(0) += 1;
84 }
85 map
86 }
87
88 fn is_simple_expr(&self) -> bool {
89 self.segments.iter().all(|seg| match seg {
90 Segment::Literal(_) => true,
91 Segment::Formatted(FormattedSegment { segment, .. }) => {
92 matches!(segment, FormattedSegmentType::Ident(_))
93 }
94 })
95 }
96
97 fn try_to_string(&self) -> Option<TokenStream> {
101 let mut single_dynamic = None;
102 for segment in &self.segments {
103 match segment {
104 Segment::Literal(literal) => {
105 if !literal.is_empty() {
106 return None;
107 }
108 }
109 Segment::Formatted(FormattedSegment {
110 segment,
111 format_args,
112 }) => {
113 if format_args.is_empty() {
114 match single_dynamic {
115 Some(current_string) => {
116 single_dynamic =
117 Some(quote!(#current_string + &(#segment).to_string()));
118 }
119 None => {
120 single_dynamic = Some(quote!((#segment).to_string()));
121 }
122 }
123 } else {
124 return None;
125 }
126 }
127 }
128 }
129 single_dynamic
130 }
131
132 pub fn to_string_with_quotes(&self) -> String {
134 self.source.to_token_stream().to_string()
135 }
136
137 fn from_raw(input: &str) -> Result<Vec<Segment>> {
139 let mut chars = input.chars().peekable();
140 let mut segments = Vec::new();
141 let mut current_literal = String::new();
142 while let Some(c) = chars.next() {
143 if c == '{' {
144 if let Some(c) = chars.next_if(|c| *c == '{') {
145 current_literal.push(c);
146 continue;
147 }
148 if !current_literal.is_empty() {
149 segments.push(Segment::Literal(current_literal));
150 }
151 current_literal = String::new();
152 let mut current_captured = String::new();
153 while let Some(c) = chars.next() {
154 if c == ':' {
155 if chars.next_if(|c| *c == ':').is_some() {
157 current_captured.push_str("::");
158 continue;
159 }
160 let mut current_format_args = String::new();
161 for c in chars.by_ref() {
162 if c == '}' {
163 segments.push(Segment::Formatted(FormattedSegment {
164 format_args: current_format_args,
165 segment: FormattedSegmentType::parse(¤t_captured)?,
166 }));
167 break;
168 }
169 current_format_args.push(c);
170 }
171 break;
172 }
173 if c == '}' {
174 segments.push(Segment::Formatted(FormattedSegment {
175 format_args: String::new(),
176 segment: FormattedSegmentType::parse(¤t_captured)?,
177 }));
178 break;
179 }
180 current_captured.push(c);
181 }
182 } else {
183 if '}' == c {
184 if let Some(c) = chars.next_if(|c| *c == '}') {
185 current_literal.push(c);
186 continue;
187 } else {
188 return Err(Error::new(
189 Span::call_site(),
190 "unmatched closing '}' in format string",
191 ));
192 }
193 }
194 current_literal.push(c);
195 }
196 }
197
198 if !current_literal.is_empty() {
199 segments.push(Segment::Literal(current_literal));
200 }
201
202 Ok(segments)
203 }
204}
205
206impl ToTokens for IfmtInput {
207 fn to_tokens(&self, tokens: &mut TokenStream) {
208 if let Some(static_str) = self.to_static() {
210 return quote_spanned! { self.span() => #static_str }.to_tokens(tokens);
211 }
212
213 if !cfg!(debug_assertions) {
215 if let Some(single_dynamic) = self.try_to_string() {
216 tokens.extend(single_dynamic);
217 return;
218 }
219 }
220
221 if self.is_simple_expr() {
223 let raw = &self.source;
224 tokens.extend(quote! {
225 ::std::format!(#raw)
226 });
227 return;
228 }
229
230 let mut format_literal = String::new();
232 let mut expr_counter = 0;
233 for segment in self.segments.iter() {
234 match segment {
235 Segment::Literal(s) => format_literal += &s.replace('{', "{{").replace('}', "}}"),
236 Segment::Formatted(FormattedSegment { format_args, .. }) => {
237 format_literal += "{";
238 format_literal += &expr_counter.to_string();
239 expr_counter += 1;
240 format_literal += ":";
241 format_literal += format_args;
242 format_literal += "}";
243 }
244 }
245 }
246
247 let span = self.span();
248
249 let positional_args = self.segments.iter().filter_map(|seg| {
250 if let Segment::Formatted(FormattedSegment { segment, .. }) = seg {
251 let mut segment = segment.clone();
252 if let FormattedSegmentType::Ident(ident) = &mut segment {
254 ident.set_span(span);
255 }
256 Some(segment)
257 } else {
258 None
259 }
260 });
261
262 quote_spanned! {
263 span =>
264 ::std::format!(
265 #format_literal
266 #(, #positional_args)*
267 )
268 }
269 .to_tokens(tokens)
270 }
271}
272
273#[derive(Debug, PartialEq, Eq, Clone, Hash)]
274pub enum Segment {
275 Literal(String),
276 Formatted(FormattedSegment),
277}
278
279impl Segment {
280 pub fn is_literal(&self) -> bool {
281 matches!(self, Segment::Literal(_))
282 }
283
284 pub fn is_formatted(&self) -> bool {
285 matches!(self, Segment::Formatted(_))
286 }
287}
288
289#[derive(Debug, PartialEq, Eq, Clone, Hash)]
290pub struct FormattedSegment {
291 pub format_args: String,
292 pub segment: FormattedSegmentType,
293}
294
295impl ToTokens for FormattedSegment {
296 fn to_tokens(&self, tokens: &mut TokenStream) {
297 let (fmt, seg) = (&self.format_args, &self.segment);
298 let fmt = format!("{{0:{fmt}}}");
299 tokens.append_all(quote! {
300 format!(#fmt, #seg)
301 });
302 }
303}
304
305#[derive(Debug, PartialEq, Eq, Clone, Hash)]
306pub enum FormattedSegmentType {
307 Expr(Box<Expr>),
308 Ident(Ident),
309}
310
311impl FormattedSegmentType {
312 fn parse(input: &str) -> Result<Self> {
313 if let Ok(ident) = parse_str::<Ident>(input) {
314 if ident == input {
315 return Ok(Self::Ident(ident));
316 }
317 }
318 if let Ok(expr) = parse_str(input) {
319 Ok(Self::Expr(Box::new(expr)))
320 } else {
321 Err(Error::new(
322 Span::call_site(),
323 "Expected Ident or Expression",
324 ))
325 }
326 }
327}
328
329impl ToTokens for FormattedSegmentType {
330 fn to_tokens(&self, tokens: &mut TokenStream) {
331 match self {
332 Self::Expr(expr) => expr.to_tokens(tokens),
333 Self::Ident(ident) => ident.to_tokens(tokens),
334 }
335 }
336}
337
338impl Parse for IfmtInput {
339 fn parse(input: ParseStream) -> Result<Self> {
340 let source: LitStr = input.parse()?;
341 Self::new_litstr(source)
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use prettier_please::PrettyUnparse;
349
350 #[test]
351 fn raw_tokens() {
352 let input = syn::parse2::<IfmtInput>(quote! { r#"hello world"# }).unwrap();
353 println!("{}", input.to_token_stream().pretty_unparse());
354 assert_eq!(input.source.value(), "hello world");
355 assert_eq!(input.to_string_with_quotes(), "r#\"hello world\"#");
356 }
357
358 #[test]
359 fn segments_parse() {
360 let input: IfmtInput = parse_quote! { "blah {abc} {def}" };
361 assert_eq!(
362 input.segments,
363 vec![
364 Segment::Literal("blah ".to_string()),
365 Segment::Formatted(FormattedSegment {
366 format_args: String::new(),
367 segment: FormattedSegmentType::Ident(Ident::new("abc", Span::call_site()))
368 }),
369 Segment::Literal(" ".to_string()),
370 Segment::Formatted(FormattedSegment {
371 format_args: String::new(),
372 segment: FormattedSegmentType::Ident(Ident::new("def", Span::call_site()))
373 }),
374 ]
375 );
376 }
377
378 #[test]
379 fn printing_raw() {
380 let input = syn::parse2::<IfmtInput>(quote! { "hello {world}" }).unwrap();
381 println!("{}", input.to_string_with_quotes());
382
383 let input = syn::parse2::<IfmtInput>(quote! { "hello {world} {world} {world}" }).unwrap();
384 println!("{}", input.to_string_with_quotes());
385
386 let input = syn::parse2::<IfmtInput>(quote! { "hello {world} {world} {world()}" }).unwrap();
387 println!("{}", input.to_string_with_quotes());
388
389 let input =
390 syn::parse2::<IfmtInput>(quote! { r#"hello {world} {world} {world()}"# }).unwrap();
391 println!("{}", input.to_string_with_quotes());
392 assert!(!input.is_static());
393
394 let input = syn::parse2::<IfmtInput>(quote! { r#"hello"# }).unwrap();
395 println!("{}", input.to_string_with_quotes());
396 assert!(input.is_static());
397 }
398
399 #[test]
400 fn to_static() {
401 let input = syn::parse2::<IfmtInput>(quote! { "body {{ background: red; }}" }).unwrap();
402 assert_eq!(
403 input.to_static(),
404 Some("body { background: red; }".to_string())
405 );
406 }
407}