biome_markup/
lib.rs

1use proc_macro2::{Delimiter, Group, Ident, TokenStream, TokenTree};
2use proc_macro_error::*;
3use quote::{quote, ToTokens};
4
5struct StackEntry {
6    name: Ident,
7    attributes: Vec<(Ident, TokenTree)>,
8}
9
10impl ToTokens for StackEntry {
11    fn to_tokens(&self, tokens: &mut TokenStream) {
12        let name = &self.name;
13        tokens.extend(quote! {
14            biome_console::MarkupElement::#name
15        });
16
17        if !self.attributes.is_empty() {
18            let attributes: Vec<_> = self
19                .attributes
20                .iter()
21                .map(|(key, value)| quote! { #key: (#value).into() })
22                .collect();
23
24            tokens.extend(quote! { { #( #attributes ),* } })
25        }
26    }
27}
28
29#[proc_macro]
30#[proc_macro_error]
31pub fn markup(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
32    let mut input = TokenStream::from(input).into_iter().peekable();
33    let mut stack = Vec::new();
34    let mut output = Vec::new();
35
36    while let Some(token) = input.next() {
37        match token {
38            TokenTree::Punct(punct) => match punct.as_char() {
39                '<' => {
40                    let is_closing_element = match input.peek() {
41                        Some(TokenTree::Punct(punct)) if punct.as_char() == '/' => {
42                            // SAFETY: Guarded by above call to peek
43                            input.next().unwrap();
44                            true
45                        }
46                        _ => false,
47                    };
48
49                    let name = match input.next() {
50                        Some(TokenTree::Ident(ident)) => ident,
51                        Some(token) => abort!(token.span(), "unexpected token"),
52                        None => abort_call_site!("unexpected end of input"),
53                    };
54
55                    let mut attributes = Vec::new();
56                    while let Some(TokenTree::Ident(_)) = input.peek() {
57                        // SAFETY: these panics are checked by the above call to peek
58                        let attr = match input.next().unwrap() {
59                            TokenTree::Ident(attr) => attr,
60                            _ => unreachable!(),
61                        };
62
63                        match input.next() {
64                            Some(TokenTree::Punct(punct)) => {
65                                if punct.as_char() != '=' {
66                                    abort!(punct.span(), "unexpected token");
67                                }
68                            }
69                            Some(token) => abort!(token.span(), "unexpected token"),
70                            None => abort_call_site!("unexpected end of input"),
71                        }
72
73                        let value = match input.next() {
74                            Some(TokenTree::Literal(value)) => TokenTree::Literal(value),
75                            Some(TokenTree::Group(group)) => {
76                                TokenTree::Group(Group::new(Delimiter::None, group.stream()))
77                            }
78                            Some(token) => abort!(token.span(), "unexpected token"),
79                            None => abort_call_site!("unexpected end of input"),
80                        };
81
82                        attributes.push((attr, value));
83                    }
84
85                    let is_self_closing = match input.next() {
86                        Some(TokenTree::Punct(punct)) => match punct.as_char() {
87                            '>' => false,
88                            '/' if !is_closing_element => {
89                                match input.next() {
90                                    Some(TokenTree::Punct(punct)) if punct.as_char() == '>' => {}
91                                    Some(token) => abort!(token.span(), "unexpected token"),
92                                    None => abort_call_site!("unexpected end of input"),
93                                }
94                                true
95                            }
96                            _ => abort!(punct.span(), "unexpected token"),
97                        },
98                        Some(token) => abort!(token.span(), "unexpected token"),
99                        None => abort_call_site!("unexpected end of input"),
100                    };
101
102                    if !is_closing_element {
103                        stack.push(StackEntry {
104                            name: name.clone(),
105                            attributes: attributes.clone(),
106                        });
107                    } else if let Some(top) = stack.last() {
108                        // Only verify the coherence of the top element on the
109                        // stack with a closing element, skip over the check if
110                        // the stack is empty as that error will be handled
111                        // when the top element gets popped off the stack later
112                        let name_str = name.to_string();
113                        let top_str = top.name.to_string();
114                        if name_str != top_str {
115                            abort!(
116                                name.span(), "closing element mismatch";
117                                close = "found closing element {}", name_str;
118                                open = top.name.span() => "expected {}", top_str
119                            );
120                        }
121                    }
122
123                    if (is_closing_element || is_self_closing) && stack.pop().is_none() {
124                        abort!(name.span(), "unexpected closing element");
125                    }
126                }
127                _ => {
128                    abort!(punct.span(), "unexpected token");
129                }
130            },
131            TokenTree::Literal(literal) => {
132                let elements: Vec<_> = stack
133                    .iter()
134                    .map(|entry| {
135                        quote! { #entry }
136                    })
137                    .collect();
138
139                output.push(quote! {
140                    biome_console::MarkupNode {
141                        elements: &[ #( #elements ),* ],
142                        content: &(#literal),
143                    }
144                });
145            }
146            TokenTree::Group(group) => match group.delimiter() {
147                Delimiter::Brace => {
148                    let elements: Vec<_> = stack.iter().map(|entry| quote! { #entry }).collect();
149
150                    let body = group.stream();
151                    output.push(quote! {
152                        biome_console::MarkupNode {
153                            elements: &[ #( #elements ),* ],
154                            content: &(#body) as &dyn biome_console::fmt::Display,
155                        }
156                    });
157                }
158                _ => abort!(group.span(), "unexpected token"),
159            },
160            TokenTree::Ident(_) => abort!(token.span(), "unexpected token"),
161        }
162    }
163
164    if let Some(top) = stack.pop() {
165        abort!(top.name.span(), "unclosed element");
166    }
167
168    quote! { biome_console::Markup(&[ #( #output ),* ]) }.into()
169}