pinkie_parser/
lib.rs

1use proc_macro2::{Delimiter, TokenStream, TokenTree};
2
3pub struct CssData {
4    pub css: String,
5    #[cfg(feature = "spans")]
6    pub spans: Vec<(usize, proc_macro2::Span)>,
7}
8
9/// Naively reimplement `TokenStream::to_string` with a few tweaks for
10/// CSS-significant (lack of) whitespace in a couple of specific places.
11///
12/// Additionally, collect spans along with their offsets in the output string
13/// for pretty nice errors for the amount of effor it took.
14pub fn parse(input: TokenStream) -> CssData {
15    let mut data = CssData {
16        css: String::new(),
17        #[cfg(feature = "spans")]
18        spans: Vec::new(),
19    };
20    parse_recursive(input, &mut data);
21    data
22}
23
24fn parse_recursive(input: TokenStream, out: &mut CssData) {
25    let mut input = input.into_iter().peekable();
26
27    while let Some(tree) = input.next() {
28        match tree {
29            TokenTree::Punct(punct) => {
30                #[cfg(feature = "spans")]
31                let pos = out.css.len();
32
33                let ch = punct.as_char();
34                out.css.push(ch);
35                if !matches!(ch, '.' | '#' | '-' | '@' | ':' | '&') {
36                    #[cfg(feature = "spans")]
37                    out.spans.push((pos, punct.span()));
38
39                    out.css.push(' ');
40                }
41            }
42            TokenTree::Ident(ident) => {
43                #[cfg(feature = "spans")]
44                out.spans.push((out.css.len(), ident.span()));
45
46                out.css.push_str(ident.to_string().trim_start_matches("r#"));
47
48                // allow kebab-case, pseudo-classes and css function calls
49                let next = input.peek();
50                if !matches!(next, Some(TokenTree::Punct(p)) if matches!(p.as_char(), '-' | ':'))
51                    && !matches!(next, Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis)
52                {
53                    out.css.push(' ');
54                }
55            }
56            TokenTree::Group(group) => {
57                let (open, close) = match group.delimiter() {
58                    Delimiter::Brace => ("{ ", "} "),
59                    Delimiter::Parenthesis => ("( ", ") "),
60                    Delimiter::Bracket => ("[ ", "] "),
61                    Delimiter::None => ("", ""),
62                };
63
64                #[cfg(feature = "spans")]
65                out.spans.push((out.css.len(), group.span_open()));
66
67                out.css.push_str(open);
68                parse_recursive(group.stream(), out);
69
70                #[cfg(feature = "spans")]
71                out.spans.push((out.css.len(), group.span_close()));
72
73                out.css.push_str(close);
74            }
75            TokenTree::Literal(lit) => {
76                #[cfg(feature = "spans")]
77                out.spans.push((out.css.len(), lit.span()));
78
79                let str = lit.to_string();
80                if str.starts_with('r') {
81                    // only usage of syn, can maybe drop?.
82                    if let Ok(str) = syn::parse_str::<syn::LitStr>(&str) {
83                        // replace newlines to keep simple span mapping
84                        let str = str.value().replace(|ch| ch == '\n' || ch == '\r', " ");
85                        out.css.push_str(&str);
86                        out.css.push(' ');
87                        continue;
88                    }
89                }
90                out.css.push_str(&str);
91                out.css.push(' ');
92            }
93        }
94    }
95}