1use proc_macro::{token_stream, Delimiter, TokenStream, TokenTree};
2use std::iter::Peekable;
3
4#[macro_use]
5mod macros;
6
7extern crate proc_macro;
8
9type StructName = String;
10#[derive(Default)]
11struct StructInfo {
12 visibility: Option<String>,
13 node: Option<String>,
14 attributes: Vec<String>,
15 vars: Vec<VarInfo>,
16}
17type ClassName = String;
18struct ClassInfo {
19 visibility: Option<String>,
20 attributes: Vec<String>,
21 vars: Vec<VarInfo>,
22}
23type StyleInfo = (Vec<(StructName, StructInfo)>, Vec<(ClassName, ClassInfo)>);
24type AttributeName = String;
25type AttributeValue = String;
26
27#[proc_macro]
131pub fn html(input: TokenStream) -> TokenStream {
132 let mut tokens = input.into_iter().peekable();
133
134 let mut styles: (Vec<(String, StructInfo)>, Vec<(String, ClassInfo)>) =
135 match parse_head(&mut tokens) {
136 Ok(styles) => styles,
137 Err(err) => return err,
138 };
139
140 let root = match get_root_tag(&mut tokens) {
141 Ok(root_tag) => root_tag,
142 Err(err) => return err,
143 };
144
145 if let Some((root_tag, _root_attributes)) = &root {
146 if !styles
147 .0
148 .iter()
149 .any(|struct_style| &struct_style.0 == root_tag)
150 {
151 styles.0.push((root_tag.clone(), StructInfo::default()));
152 }
153 }
154
155 let mut result = implement_styles(&styles);
156
157 if let Some((root_tag, mut root_attributes)) = root {
158 let root_visibility = styles
159 .0
160 .iter()
161 .find(|struct_style| struct_style.0 == root_tag)
162 .map(|struct_style| struct_style.1.visibility.clone())
163 .unwrap_or_default();
164
165 match parse_root(
166 &mut tokens,
167 &root_tag,
168 &mut root_attributes,
169 root_visibility.as_deref(),
170 &styles.1.iter().map(|class| class.0.as_str()).collect(),
171 ) {
172 Ok(body_result) => result.push_str(&body_result),
173 Err(err) => return err,
174 };
175 }
176
177 match result.parse() {
178 Ok(result) => result,
179 Err(err) => format_compile_error!("Could not parse result: {err}"),
180 }
181}
182#[allow(clippy::too_many_lines)]
183fn parse_head(tokens: &mut Peekable<token_stream::IntoIter>) -> Result<StyleInfo, TokenStream> {
184 let mut structs: Vec<(StructName, StructInfo)> = Vec::new();
185 let mut classes: Vec<(ClassName, ClassInfo)> = Vec::new();
186 let mut visibility = None;
187
188 if !peek_matches_tag(tokens.clone(), "head") {
189 return Ok((structs, classes));
190 }
191 assert_next_tag(tokens, "head")?;
192 if peek_matches_tag(tokens.clone(), "style") {
193 tokens.nth(2);
194
195 while let Some(token) = tokens.peek() {
196 match token {
197 TokenTree::Punct(p) => match p.as_char() {
198 '<' => {
199 break;
200 }
201 '.' => {
203 tokens.next();
204 let class_name = assert_next_token!(tokens, Ident, Err).to_string();
205
206 let TokenTree::Group(group) =
207 assert_next_token!(tokens, Group, Delimiter::Brace, Err)
208 else {
209 return Err(format_compile_error!("Not reachable"));
210 };
211 let mut group_tokens = group.stream().into_iter().peekable();
212 let mut attributes = vec![];
213 let mut vars = vec![];
214 while group_tokens.peek().is_some() {
215 if peek_matches_token!(group_tokens, Punct, "$") {
216 vars.push(parse_local_var(&mut group_tokens)?);
217 } else {
218 let attribute = parse_style_attribute(&mut group_tokens, "$")?;
219 assert_next_token!(group_tokens, Punct, ";", Err);
220 attributes.push(attribute);
221 }
222 }
223 classes.push((
224 class_name,
225 ClassInfo {
226 visibility: visibility.take(),
227 attributes,
228 vars,
229 },
230 ));
231 }
232 unexpected => {
233 return Err(format_compile_error!(
234 "Unexpected value {unexpected} in style"
235 ))
236 }
237 },
238 TokenTree::Ident(ident) if ident.to_string() == "pub" => {
240 let ident = ident.to_string();
241 tokens.next();
242 visibility = match tokens.peek() {
243 Some(TokenTree::Group(group))
244 if group.delimiter() == Delimiter::Parenthesis =>
245 {
246 let group = group.to_string();
247 tokens.next();
248 Some(format!("{ident}{group}"))
249 }
250 _ => Some(ident),
251 }
252 }
253 TokenTree::Ident(struct_name) => {
255 let struct_name = struct_name.to_string();
256 tokens.next();
257
258 let TokenTree::Group(group) =
259 assert_next_token!(tokens, Group, Delimiter::Brace, Err)
260 else {
261 return Err(format_compile_error!("Not reachable"));
262 };
263 let mut group_tokens = group.stream().into_iter().peekable();
264 let mut node = None;
265 let mut attributes = vec![];
266 let mut vars = vec![];
267 while group_tokens.peek().is_some() {
268 if peek_matches_token!(group_tokens, Punct, "$") {
269 vars.push(parse_local_var(&mut group_tokens)?);
270 } else {
271 let attribute = parse_style_attribute(&mut group_tokens, "")?;
272 assert_next_token!(group_tokens, Punct, ";", Err);
273 if &attribute[.."Node".len()] == "Node" {
274 node = Some(attribute);
275 } else {
276 attributes.push(attribute);
277 }
278 }
279 }
280 structs.push((
281 struct_name,
282 StructInfo {
283 visibility: visibility.take(),
284 node,
285 attributes,
286 vars,
287 },
288 ));
289 }
290 unexpected => {
291 return Err(format_compile_error!(
292 "Unexpected value {unexpected} in style"
293 ))
294 }
295 }
296 }
297
298 assert_next_end_tag(tokens, "style")?;
299 }
300 assert_next_end_tag(tokens, "head")?;
301
302 Ok((structs, classes))
303}
304struct VarInfo {
305 name: String,
306 value: String,
307}
308fn parse_local_var(tokens: &mut Peekable<token_stream::IntoIter>) -> Result<VarInfo, TokenStream> {
309 assert_next_token!(tokens, Punct, "$", Err);
310 let name = assert_next_token!(tokens, Ident, Err).to_string();
311 assert_next_token!(tokens, Punct, "=", Err);
312 let value = assert_next_string_lit!(tokens, Err);
313 assert_next_token!(tokens, Punct, ";", Err);
314
315 Ok(VarInfo { name, value })
316}
317const fn delimiter_to_chars(delimiter: Delimiter) -> (char, char) {
318 match delimiter {
319 Delimiter::Brace => ('{', '}'),
320 Delimiter::Parenthesis => ('(', ')'),
321 Delimiter::Bracket => ('[', ']'),
322 Delimiter::None => (' ', ' '),
323 }
324}
325fn parse_style_attribute(
326 group_tokens: &mut Peekable<token_stream::IntoIter>,
327 attributes_prefix: &str,
328) -> Result<String, TokenStream> {
329 let mut token_stack = vec![];
330 let mut group_delimiter_stack = vec![];
331 let attribute = {
332 let mut collected = String::new();
333 let mut last_was_ident = false;
334 while token_stack.last().is_some()
335 || (group_tokens.peek().is_some() && !peek_matches_token!(group_tokens, Punct, ";"))
336 {
337 let tokens = token_stack.last_mut().unwrap_or(group_tokens);
338 let Some(next_token) = tokens.peek() else {
339 unreachable!()
340 };
341 match next_token {
342 TokenTree::Group(group) => {
343 let delimiter = group.delimiter();
344 group_delimiter_stack.push(delimiter);
345 collected.push(delimiter_to_chars(delimiter).0);
346
347 let group_tokens = group.stream().into_iter().peekable();
348 tokens.next();
349 token_stack.push(group_tokens);
350 }
351 TokenTree::Punct(punct) if punct.as_char() == '$' => {
352 tokens.next();
353 let var = assert_next_token!(tokens, Ident, Err);
354 collected.push_str(&format!(
355 "{attributes_prefix}attributes.get(\"{var}\").unwrap_or(&{var})"
356 ));
357 }
358 token => {
359 if let TokenTree::Ident(_) = token {
360 if last_was_ident {
361 collected.push(' ');
362 }
363 last_was_ident = true;
364 } else {
365 last_was_ident = false;
366 }
367
368 let token = token.to_string();
369 tokens.next();
370 collected.push_str(&token);
371 }
372 }
373 while token_stack
374 .last_mut()
375 .is_some_and(|tokens| tokens.peek().is_none())
376 {
377 if let Some(delimiter) = group_delimiter_stack.pop() {
378 collected.push(delimiter_to_chars(delimiter).1);
379 }
380
381 token_stack.pop();
382 }
383 }
384 collected
385 };
386 Ok(attribute)
387}
388fn implement_styles(styles: &StyleInfo) -> String {
389 let mut result = String::new();
390 let (structs, classes) = styles;
391
392 for (struct_name, struct_info) in structs {
393 let visibility = struct_info
394 .visibility
395 .clone()
396 .map_or_else(<_>::default, |visibility| format!("{visibility} "));
397
398 let node = struct_info
399 .node
400 .as_ref()
401 .map_or_else(|| "Node::default()".to_owned(), ToOwned::to_owned);
402
403 let mut attributes = String::new();
404
405 if !struct_info.attributes.is_empty() {
406 attributes.push_str("me");
407 for attribute in &struct_info.attributes {
408 attributes.push_str(&format!(".insert({attribute})"));
409 }
410 attributes.push(';');
411 }
412
413 let vars = {
414 let mut vars = String::new();
415 for var in &struct_info.vars {
416 vars.push_str(&format!(
417 "let {}: String = String::from(\"{}\");\n",
418 var.name, var.value
419 ));
420 }
421 vars
422 };
423
424 result.push_str(&format!(
426 "
427 #[derive(Component)]\n
428 #[allow(dead_code)]\n
429 {visibility}struct {struct_name};\n
430 impl {struct_name} {{\n
431 #![allow(unused_variables)]\n
432
433 {visibility}fn spawn_as_child<'a>(parent: &'a mut ChildBuilder<'_>, attributes: &std::collections::HashMap<String,String>) -> EntityCommands<'a> {{\n
434 parent.spawn((Self, Self::get_node(attributes)))\n
435 }}\n
436
437 {visibility}fn get_node(attributes: &std::collections::HashMap<String,String>) -> Node {{\n
438 {vars}\n
439 {node}\n
440 }}\n
441
442 {visibility}fn apply_attributes<'a>(mut me: EntityCommands<'a>, asset_server: &'a Res<AssetServer>, attributes: &std::collections::HashMap<String,String>) -> EntityCommands<'a> {{\n
443 {vars}\n
444 {attributes}\n
445 me\n
446 }}\n
447 }}"
448 ));
449 }
450 for (class_name, class_info) in classes {
451 let macro_name = format!("apply_{class_name}_class");
452
453 let (visibility, attributes, vars) = {
454 let visibility = class_info
455 .visibility
456 .clone()
457 .map_or_else(<_>::default, |visibility| {
458 format!("{visibility} use {macro_name};")
459 });
460
461 let mut attributes = String::new();
462
463 if !class_info.attributes.is_empty() {
464 attributes.push_str("element");
465 for attribute in &class_info.attributes {
466 attributes.push_str(&format!(".insert({attribute})"));
467 }
468 attributes.push(';');
469 }
470
471 let vars = {
472 let mut vars = String::new();
473 for var in &class_info.vars {
474 vars.push_str(&format!(
475 "let {}: String = String::from(\"{}\");\n",
476 var.name, var.value
477 ));
478 }
479 vars
480 };
481
482 (visibility, attributes, vars)
483 };
484
485 result.push_str(&format!(
486 "
487 #[allow(unused_macros)]
488 macro_rules! {macro_name} {{\n
489 ($element:expr, $asset_server:ident, $attributes:ident) => {{{{\n
490 #[allow(unused_variables)]
491 let asset_server = &$asset_server;
492 {vars}
493 #[allow(unused_mut)]
494 let mut element = $element;\n
495 {attributes}
496 element\n
497 }}}}\n
498 }}\n
499 {visibility}"
500 ));
501 }
502
503 result
504}
505fn get_root_tag(
506 tokens: &mut Peekable<token_stream::IntoIter>,
507) -> Result<Option<(String, Vec<(AttributeName, AttributeValue)>)>, TokenStream> {
508 if tokens.peek().is_some() {
509 assert_next_token!(tokens, Punct, "<", Err);
510 let Some(tag_name) = tokens.next().map(|token| token.to_string()) else {
511 return Err(format_compile_error!("Expected root tag name"));
512 };
513 let attribute_info = parse_attributes(tokens)?;
514 assert_next_token!(tokens, Punct, ">", Err);
515 Ok(Some((tag_name, attribute_info)))
516 } else {
517 Ok(None)
518 }
519}
520fn parse_root(
521 tokens: &mut Peekable<token_stream::IntoIter>,
522 root_tag: &str,
523 root_attributes: &mut Vec<(AttributeName, AttributeValue)>,
524 root_visibility: Option<&str>,
525 implemented_classes: &Vec<&str>,
526) -> Result<String, TokenStream> {
527 let root_visibility = root_visibility.unwrap_or_default();
528 let classes = classes_from_attributes(root_attributes);
529 let (apply_classes, apply_classes_end) = get_apply_classes_code(implemented_classes, classes)?;
530 let attribute_tuples = get_attribute_tuples(root_attributes);
531
532 let is_literal = peek_matches_token!(tokens, Literal);
533 let literal_value = if is_literal {
534 Some(assert_next_string_lit!(tokens, Err))
535 } else {
536 None
537 };
538 let literal_value_code = literal_value
539 .map(|literal_value| format!(", Text::from(\"{literal_value}\")"))
540 .unwrap_or_default();
541
542 let mut result = format!("
543 impl {root_tag}{{\n
544 {root_visibility}fn spawn_as_root(mut commands: Commands, asset_server: Res<AssetServer>) {{\n
545 let attributes: std::collections::HashMap<String, String> = std::collections::HashMap::from([{attribute_tuples}]);\n
546 let entity = commands.spawn((Self, Self::get_node(&attributes){literal_value_code})).id();\n
547 let mut me = commands.entity(entity);\n
548 Self::spawn_children(&mut me, &asset_server, &attributes);\n
549 {apply_classes}Self::apply_attributes(me, &asset_server, &attributes){apply_classes_end};\n
550 }}\n
551 {root_visibility}fn spawn<'a>(parent: &'a mut ChildBuilder<'_>, asset_server: &'a Res<AssetServer>, new_attributes: &std::collections::HashMap<String,String>) -> EntityCommands<'a> {{\n
552 let mut attributes: std::collections::HashMap<String, String> = std::collections::HashMap::from([{attribute_tuples}]);\n
553 for (k, v) in new_attributes {{\n
554 attributes.insert(k.clone(), v.clone());\n
555 }}\n
556 let mut me = {apply_classes}Self::apply_attributes(\n
557 parent.spawn((Self, Self::get_node(&attributes){literal_value_code})),\n
558 asset_server,\n
559 &attributes,\n
560 ){apply_classes_end};\n
561 Self::spawn_children(&mut me, asset_server, &attributes);\n
562 me\n
563 }}\n
564 {root_visibility}fn spawn_children(me: &mut EntityCommands<'_>, asset_server: &Res<AssetServer>, new_attributes: &std::collections::HashMap<String,String>) {{\n
565 me.with_children(|parent| {{\n");
566 if !is_literal {
567 while tokens
568 .clone()
569 .nth(1)
570 .is_some_and(|token| token.to_string() != "/")
571 {
572 let tag_result = parse_tag(tokens, implemented_classes)?;
573 result.push_str(&tag_result);
574 }
575 }
576 assert_next_end_tag(tokens, root_tag)?;
577 result.push_str("});\n}\n}");
578
579 Ok(result)
580}
581
582fn get_attribute_tuples(attributes: &[(String, String)]) -> String {
583 let attribute_tuples = attributes
584 .iter()
585 .fold(String::new(), |mut result, attribute| {
586 result.push_str(&format!(
587 "(String::from(\"{}\"), String::from(\"{}\")),",
588 attribute.0, attribute.1
589 ));
590 result
591 });
592 attribute_tuples
593}
594#[allow(clippy::too_many_lines)]
595fn parse_tag(
596 tokens: &mut Peekable<impl Iterator<Item = TokenTree> + Clone>,
597 implemented_classes: &Vec<&str>,
598) -> Result<String, TokenStream> {
599 let mut result = String::new();
600
601 assert_next_token!(tokens, Punct, "<", Err);
602 let (struct_name, classes, attributes) = if let Some(TokenTree::Ident(ident)) = tokens.peek() {
603 let ident = ident.to_string();
604 let struct_name = if tokens
605 .clone()
606 .nth(1)
607 .is_some_and(|token| token.to_string() == "=")
608 {
609 None
610 } else {
611 tokens.next();
612 Some(ident)
613 };
614 let mut attributes = parse_attributes(tokens)?;
615 let classes = classes_from_attributes(&mut attributes);
616
617 (struct_name, classes, attributes)
618 } else {
619 (None, None, vec![])
620 };
621 let is_self_closing = match tokens.next() {
622 Some(TokenTree::Punct(punct)) if [">", "/"].contains(&punct.to_string().as_ref()) => {
623 punct.to_string() == "/"
624 }
625 _ => {
626 return Err(format_compile_error!(
627 "Expected > or /{}.",
628 struct_name.map_or_else(String::new, |struct_name| format!(" after {struct_name}"))
629 ));
630 }
631 };
632
633 let (apply_classes, apply_classes_end) = get_apply_classes_code(implemented_classes, classes)?;
634
635 let attribute_tuples = get_attribute_tuples(&attributes);
636 if is_self_closing {
637 let Some(struct_name) = struct_name else {
639 return Err(format_compile_error!("Self closing tag must have a name"));
640 };
641 result.push_str(&format!("{{\n
642 let mut attributes: std::collections::HashMap<String, String> = std::collections::HashMap::<String, String>::from([{attribute_tuples}]);\n
643 for (k, v) in new_attributes {{\n
644 attributes.insert(k.clone(), v.clone());\n
645 }}\n
646 {apply_classes}{struct_name}::apply_attributes({struct_name}::spawn(parent, asset_server, &attributes), asset_server, &attributes){apply_classes_end}\n
647 }};"));
648 } else {
649 if !(peek_matches_token!(tokens, Literal) || peek_matches_token!(tokens, Punct, "<")) {
651 return Err(format_compile_error!("Expected end tag"));
652 }
653
654 result.push_str(&struct_name.as_ref().map_or_else(
655 || format!("{{{apply_classes}parent.spawn(Node::default()){apply_classes_end}"),
656 |struct_name| {
657 format!("{{\n
658 let mut attributes = std::collections::HashMap::<String, String>::from([{attribute_tuples}]);\n
659 for (k, v) in new_attributes {{\n
660 attributes.insert(k.clone(), v.clone());\n
661 }}\n
662 {apply_classes}{struct_name}::apply_attributes({struct_name}::spawn_as_child(parent, &attributes), asset_server, &attributes){apply_classes_end}")
663 },
664 ));
665 if let Some(TokenTree::Literal(literal)) = tokens.peek() {
666 let literal = literal.to_string();
667 tokens.next();
668 result.push_str(&format!(".insert(Text::from({literal}));"));
669 } else {
670 result.push_str(".with_children(|parent| {\n");
671 while tokens
672 .clone()
673 .nth(1)
674 .is_some_and(|token| token.to_string() != "/")
675 {
676 match parse_tag(tokens, implemented_classes) {
677 Ok(child_result) => {
678 result.push_str(&child_result);
679 }
680 error => {
681 return error;
682 }
683 }
684 }
685 result.push_str("});");
686 }
687 result.push('}');
688 assert_next_token!(tokens, Punct, "<", Err);
689 assert_next_token!(tokens, Punct, "/", Err);
690 if peek_matches_token!(tokens, Ident) {
691 let tag = collect_until_token!(tokens, Punct, ">");
692 if struct_name
693 .clone()
694 .is_none_or(|struct_name| tag != struct_name)
695 {
696 return Err(format_compile_error!(
697 "Expected </{}>",
698 struct_name.unwrap_or_default()
699 ));
700 }
701 }
702 }
703 assert_next_token!(tokens, Punct, ">", Err);
704
705 Ok(result)
706}
707
708fn classes_from_attributes(attributes: &mut Vec<(String, String)>) -> Option<Vec<String>> {
709 let class_info = attributes
710 .iter()
711 .position(|attribute| attribute.0 == "class");
712 let classes = class_info.map(|class_info| {
713 attributes
714 .swap_remove(class_info)
715 .1
716 .split_whitespace()
717 .map(str::to_string)
718 .collect::<Vec<_>>()
719 });
720 classes
721}
722fn get_apply_classes_code(
723 implemented_classes: &Vec<&str>,
724 classes: Option<Vec<String>>,
725) -> Result<(String, String), TokenStream> {
726 let (apply_classes, apply_classes_end) = if let Some(classes) = classes {
727 let (mut apply_classes, mut apply_classes_end) = (String::new(), String::new());
728 for class in classes {
729 if !implemented_classes.contains(&class.as_ref()) {
730 return Err(format_compile_error!(
731 "Class \\\"{class}\\\" does not exist"
732 ));
733 }
734
735 apply_classes.push_str(&format!("apply_{class}_class!("));
736 apply_classes_end.push_str(", asset_server, attributes)");
737 }
738
739 (apply_classes, apply_classes_end)
740 } else {
741 (String::new(), String::new())
742 };
743 Ok((apply_classes, apply_classes_end))
744}
745fn peek_matches_tag(mut tokens: impl Iterator<Item = TokenTree>, expected: &str) -> bool {
746 if let (Some(TokenTree::Punct(p1)), Some(TokenTree::Ident(tag)), Some(TokenTree::Punct(p2))) =
747 (tokens.next(), tokens.next(), tokens.next())
748 {
749 p1.as_char() == '<' && p2.as_char() == '>' && tag.to_string() == expected
750 } else {
751 false
752 }
753}
754fn assert_next_end_tag(
755 tokens: &mut Peekable<impl Iterator<Item = TokenTree> + Clone>,
756 expected: &str,
757) -> Result<(), TokenStream> {
758 let first = tokens.peek().map(ToString::to_string);
759 let second = tokens.clone().nth(1).map(|token| token.to_string());
760 match (tokens.next(), tokens.next(), tokens.next(), tokens.next()) {
761 (
762 Some(TokenTree::Punct(p1)),
763 Some(TokenTree::Punct(p2)),
764 Some(TokenTree::Ident(tag)),
765 Some(TokenTree::Punct(p3)),
766 ) if p1.as_char() == '<' && p2.as_char() == '/' && p3.as_char() == '>' => {
767 if tag.to_string() != expected {
768 return Err(format_compile_error!(
769 "Expected </{expected}>, but received </{tag}>",
770 ));
771 }
772 Ok(())
773 }
774 _ => {
775 if let Some(first) = first {
776 return Err(format_compile_error!(
777 "Expected </{expected}>, but received {first}{}",
778 second.unwrap_or_default()
779 ));
780 }
781 Err(format_compile_error!("Expected </{expected}>",))
782 }
783 }
784}
785fn assert_next_tag(
786 tokens: &mut Peekable<impl Iterator<Item = TokenTree> + Clone>,
787 expected: &str,
788) -> Result<(), TokenStream> {
789 match (tokens.next(), tokens.next(), tokens.next()) {
790 (Some(TokenTree::Punct(p1)), Some(TokenTree::Ident(tag)), Some(TokenTree::Punct(p2)))
791 if p1.as_char() == '<' && p2.as_char() == '>' =>
792 {
793 if tag.to_string() != expected {
794 return Err(format_compile_error!(
795 "Expected <{expected}>, but received <{tag}>",
796 ));
797 }
798 Ok(())
799 }
800 _ => Err(format_compile_error!("Expected <{expected}>",)),
801 }
802}
803fn parse_attributes(
804 tokens: &mut Peekable<impl Iterator<Item = TokenTree> + Clone>,
805) -> Result<Vec<(AttributeName, AttributeValue)>, TokenStream> {
806 let mut result = vec![];
807 while !(peek_matches_token!(tokens, Punct, ">") || peek_matches_token!(tokens, Punct, "/")) {
808 let attribute_name = assert_next_token!(tokens, Ident, Err).to_string();
809 assert_next_token!(tokens, Punct, "=", Err);
810 let attribute_value = assert_next_string_lit!(tokens, Err);
811 result.push((attribute_name, attribute_value));
812 }
813 Ok(result)
814}