1use std::fmt::Display;
2
3use proc_macro::TokenStream;
4use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
5use proc_macro_crate::{crate_name, FoundCrate};
6use proc_macro_error::abort;
7use quote::{quote, quote_spanned, ToTokens};
8use rstml::{
9 atoms::{CloseTag, OpenTag},
10 node::{
11 KeyedAttribute, KeyedAttributeValue, Node, NodeAttribute, NodeComment, NodeElement,
12 NodeFragment, NodeName, NodeText,
13 },
14 Parser, ParserConfig,
15};
16use syn::{
17 punctuated::{Pair, Punctuated},
18 spanned::Spanned,
19 ExprPath, LitStr,
20};
21use uuid::Uuid;
22
23struct HyperideGenerator {
24 bindings: TokenStream2,
25 idents: Vec<Ident>,
26 hyperide: TokenStream2,
27 in_disabled_raw: bool,
28}
29impl HyperideGenerator {
30 fn new(hyperide: TokenStream2) -> HyperideGenerator {
31 HyperideGenerator {
32 bindings: quote_spanned! {Span::call_site()=>},
33 idents: Vec::new(),
34 hyperide,
35 in_disabled_raw: false,
36 }
37 }
38
39 fn push_raw_hypertext(&mut self, to: TokenStream2) -> Ident {
40 let bind = make_ident(Span::call_site());
41 let hyperide = &self.hyperide;
42 self.bindings.extend(quote_spanned! {Span::call_site()=>
43 #[allow(unused_braces)]
44 let #bind: #hyperide::HyperText<'_> = #to;
45 });
46 self.idents.push(bind.clone());
47 bind
48 }
49
50 fn push_as_hypertext(&mut self, to: TokenStream2) -> Ident {
51 let hyperide = &self.hyperide;
52 self.push_raw_hypertext(quote_spanned! {to.span()=>
53 #hyperide::IntoHyperText::into_hyper_text(#to)
54 })
55 }
56
57 fn push_lit(&mut self, lit: &LitStr) -> Ident {
58 self.push_as_hypertext(lit.to_token_stream())
59 }
60
61 fn push_str(&mut self, str: &str, span: Span) -> Ident {
62 self.push_lit(&LitStr::new(str, span))
63 }
64
65 fn push_ref(&mut self, ident: &Ident) -> Ident {
66 self.push_raw_hypertext(quote_spanned! {Span::call_site()=>
67 std::ops::Deref::deref(&#ident).into();
68 })
69 }
70
71 fn push_nodes(&mut self, nodes: &[Node]) {
72 for node in nodes {
73 match node {
74 Node::Comment(NodeComment { value, .. }) => {
75 self.push_lit(value);
76 }
77 Node::Doctype(doctype) => {
78 self.push_str("<!DOCTYPE html>", doctype.span());
79 }
80 Node::Fragment(NodeFragment { children, .. }) => {
81 self.push_nodes(&children);
82 }
83 Node::Block(block) => {
84 self.push_as_hypertext(block.to_token_stream());
85 }
86 Node::Text(NodeText { value }) => {
87 self.push_lit(value);
88 }
89 Node::RawText(raw_text) => {
90 if !self.in_disabled_raw {
91 let best_string = raw_text.to_string_best();
92 self.push_str(&best_string, raw_text.span());
93 } else {
94 self.push_as_hypertext(raw_text.to_token_stream());
95 }
96 }
97 Node::Element(element) => self.push_element(element),
98 }
99 }
100 }
101
102 fn push_element(&mut self, element: &NodeElement) {
103 let NodeElement {
104 open_tag,
105 children,
106 close_tag,
107 } = element;
108
109 let open_ident = self.push_open_tag(open_tag);
110 self.push_nodes(&children);
111 self.in_disabled_raw = false;
112 self.push_close_tag(close_tag.as_ref(), &open_ident);
113 }
114
115 fn push_open_tag(&mut self, open_tag: &OpenTag) -> Ident {
116 let OpenTag {
117 token_lt,
118 name,
119 generics,
120 attributes,
121 end_tag,
122 } = open_tag;
123
124 if generics.lt_token.is_some() {
125 abort!(generics.lt_token.span(), "Tag must not have generics");
126 }
127
128 self.push_str("<", token_lt.span());
129
130 let open_value_ident = match name {
131 NodeName::Path(path) => {
132 let name = get_path_ident(path);
133 self.push_str(&name.to_string(), name.span())
134 }
135 NodeName::Punctuated(punct) => {
136 let name = get_punct_hypertext(punct);
138 self.push_str(&name, punct.span())
139 }
140 NodeName::Block(block) => self.push_as_hypertext(block.to_token_stream()),
141 };
142
143 for attribute in attributes {
144 self.push_str(" ", attribute.span());
145 match attribute {
146 NodeAttribute::Block(block) => {
147 self.push_as_hypertext(block.to_token_stream());
148 }
149 NodeAttribute::Attribute(keyed) => {
150 let KeyedAttribute {
151 key,
152 possible_value,
153 } = keyed;
154
155 match key {
156 NodeName::Path(path) => {
157 let name = get_path_ident(path);
158 if name == "_hr_no_raw" {
159 self.in_disabled_raw = true;
160 }
161 self.push_str(&name.to_string(), key.span());
162 }
163 NodeName::Punctuated(punct) => {
164 let name = get_punct_hypertext(punct);
166 self.push_str(&name, punct.span());
167 }
168 NodeName::Block(block) => {
169 self.push_as_hypertext(block.to_token_stream());
170 }
171 };
172 let key_ident = unsafe { self.idents.pop().unwrap_unchecked() };
174
175 match possible_value {
176 KeyedAttributeValue::Binding(binding) => {
177 abort!(
178 binding.span(),
179 "I have no idea what this is open an issue if you see it"
180 )
181 }
182 KeyedAttributeValue::Value(expr) => {
183 let hyperide = &self.hyperide;
184 let value = &expr.value;
185 self.push_as_hypertext(quote_spanned! {expr.span()=>
186 #hyperide::IntoAttrText::into_attr_text(#value, #key_ident)
187 });
188 }
189 KeyedAttributeValue::None => {
190 self.push_ref(&key_ident);
191 }
192 }
193 }
194 }
195 }
196
197 self.push_str(">", end_tag.span());
198
199 open_value_ident
200 }
201
202 fn push_close_tag(&mut self, close_tag: Option<&CloseTag>, open_ident: &Ident) {
203 if let Some(close_tag) = close_tag {
204 let CloseTag {
205 start_tag,
206 name,
207 generics,
208 token_gt,
209 } = close_tag;
210
211 if generics.lt_token.is_some() {
212 abort!(generics.lt_token.span(), "Tag must not have generics");
213 }
214
215 self.push_str("</", start_tag.span());
216
217 if name.is_wildcard() {
218 self.push_ref(open_ident);
219 } else {
220 match name {
221 NodeName::Path(path) => {
222 let name = get_path_ident(path);
223 self.push_str(&name.to_string(), name.span());
224 }
225 NodeName::Punctuated(punct) => {
226 let name = get_punct_hypertext(punct);
227 self.push_str(&name.to_string(), punct.span());
228 }
229 NodeName::Block(block) => {
230 self.push_as_hypertext(block.to_token_stream());
231 }
232 }
233 }
234
235 self.push_str(">", token_gt.span());
236 }
237 }
238}
239
240fn make_ident(span: Span) -> Ident {
241 Ident::new(
242 &format!("__hyperide_internal_{}", Uuid::new_v4().simple()),
243 span,
244 )
245}
246
247fn get_path_ident(path: &ExprPath) -> &Ident {
248 if !path.attrs.is_empty() {
249 abort!(path.span(), "Expected ident, found attribute");
250 }
251 match path.path.get_ident() {
252 Some(ident) => ident,
253 None => abort!(path.span(), "Expected ident, found path"),
254 }
255}
256
257fn get_punct_hypertext<T>(punct: &Punctuated<impl Display, T>) -> String {
258 let mut name = String::new();
259 for term in punct.pairs() {
260 match term {
261 Pair::Punctuated(term, _punct) => {
262 name.push_str(&format!("{term}"));
263 name.push('-'); }
265 Pair::End(term) => {
266 name.push_str(&format!("{term}"));
267 }
268 }
269 }
270 name
271}
272
273#[proc_macro_error::proc_macro_error]
318#[proc_macro]
319pub fn hyperide(tokens: TokenStream) -> TokenStream {
320 let Ok(hyperide) = crate_name("hyperide") else {
321 abort!(proc_macro2::TokenStream::from(tokens), "hyperide crate must be available")
322 };
323 let hyperide = match hyperide {
324 FoundCrate::Itself => quote! { ::hyperide },
325 FoundCrate::Name(name) => {
326 let ident = Ident::new(&name, Span::call_site());
327 quote! { ::#ident }
328 }
329 };
330
331 let config = ParserConfig::new()
332 .recover_block(true)
333 .always_self_closed_elements(
335 [
336 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
337 "param", "source", "track", "wbr",
338 ]
339 .into_iter()
340 .collect(),
341 )
342 .raw_text_elements(["script", "style"].into_iter().collect())
343 .element_close_wildcard(|_, close_tag| close_tag.name.is_wildcard());
344
345 let parser = Parser::new(config);
346 let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
347
348 let mut walker = HyperideGenerator::new(hyperide);
349 walker.push_nodes(&nodes);
350
351 let idents = walker.idents;
352 let bindings = walker.bindings;
353
354 let errors = errors.into_iter().map(|e| e.emit_as_expr_tokens());
355 let alloc_size = make_ident(Span::call_site());
356 let string_out = make_ident(Span::call_site());
357 let out = quote! {{
358 #(#errors;)*
359 #bindings
360 let #alloc_size = #(
361 std::ops::Deref::deref(&#idents).len()
362 )+*;
363 let mut #string_out = String::with_capacity(#alloc_size);
364 #(
365 #string_out.push_str(std::ops::Deref::deref(&#idents));
366 )*
367 #string_out
368 }};
369
370 out.into()
371}