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 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 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 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}