1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3#![deny(rustdoc::broken_intra_doc_links)]
4use proc_macro::TokenStream;
27use proc_macro2::{Delimiter, Group, Ident, Span, TokenStream as TokenStream2, TokenTree};
28use quote::{format_ident, quote};
29use swc_common::{FileName, SourceMap};
30use swc_ecma_parser::{EsSyntax, Parser, StringInput, Syntax};
31use syn::{
32 Data, DeriveInput, Expr, Fields, GenericParam, Generics, LitStr, Result, Token, Type, TypePath,
33 ext::IdentExt,
34 parse::{Nothing, Parse, ParseStream},
35 parse_macro_input, parse_quote,
36 punctuated::Punctuated,
37 spanned::Spanned,
38};
39
40const SURREAL_JS_BUNDLE: &str = include_str!("../assets/surreal.js");
41const CSS_SCOPE_INLINE_JS_BUNDLE: &str = include_str!("../assets/css-scope-inline.js");
42const SIGNALS_CORE_JS_BUNDLE: &str = include_str!("../assets/signals-core.min.js");
43const SIGNALS_ADAPTER_JS_BUNDLE: &str = include_str!("../assets/signals-adapter.js");
44const COMPONENT_JS_HELPER_FN: &str =
45 "__maud_extensions_component_requires_js_macro_in_scope_can_be_empty";
46const COMPONENT_CSS_HELPER_FN: &str =
47 "__maud_extensions_component_requires_css_macro_in_scope_can_be_empty";
48const COMPONENT_JS_MODE_ATTR: &str = "data-mx-js-mode";
49const COMPONENT_JS_RAN_ATTR: &str = "data-mx-js-ran";
50const COMPONENT_SYNTAX_ERROR: &str = "component! expects optional directives first (`@js-once` or `@js-always`) followed by exactly one top-level element with a body block, e.g. component! { @js-once article { ... } }";
51
52#[derive(Clone, Copy, PartialEq, Eq)]
53enum ComponentJsMode {
54 Always,
55 Once,
56}
57
58impl ComponentJsMode {
59 fn as_str(self) -> &'static str {
60 match self {
61 ComponentJsMode::Always => "always",
62 ComponentJsMode::Once => "once",
63 }
64 }
65}
66
67enum JsInput {
68 Literal(LitStr),
69 Tokens(TokenStream2),
70}
71
72impl Parse for JsInput {
73 fn parse(input: ParseStream) -> Result<Self> {
74 if input.peek(LitStr) {
75 let content: LitStr = input.parse()?;
76 Ok(JsInput::Literal(content))
77 } else {
78 let tokens: TokenStream2 = input.parse()?;
79 Ok(JsInput::Tokens(tokens))
80 }
81 }
82}
83
84enum CssInput {
85 Literal(LitStr),
86 Tokens(TokenStream2),
87}
88
89impl Parse for CssInput {
90 fn parse(input: ParseStream) -> Result<Self> {
91 if input.peek(LitStr) {
92 let content: LitStr = input.parse()?;
93 Ok(CssInput::Literal(content))
94 } else {
95 let tokens: TokenStream2 = input.parse()?;
96 Ok(CssInput::Tokens(tokens))
97 }
98 }
99}
100
101struct CssHelperInput {
102 helper_name: Option<LitStr>,
103 css: CssInput,
104}
105
106impl Parse for CssHelperInput {
107 fn parse(input: ParseStream) -> Result<Self> {
108 if input.peek(LitStr) && input.peek2(Token![,]) {
109 let helper_name: LitStr = input.parse()?;
110 input.parse::<Token![,]>()?;
111
112 let css = if input.peek(LitStr) {
113 CssInput::Literal(input.parse()?)
114 } else if input.peek(syn::token::Brace) {
115 let content;
116 syn::braced!(content in input);
117 CssInput::Tokens(content.parse()?)
118 } else {
119 CssInput::Tokens(input.parse()?)
120 };
121
122 if !input.is_empty() {
123 return Err(input.error("unexpected trailing tokens after named css! helper"));
124 }
125
126 Ok(Self {
127 helper_name: Some(helper_name),
128 css,
129 })
130 } else {
131 Ok(Self {
132 helper_name: None,
133 css: input.parse()?,
134 })
135 }
136 }
137}
138
139fn expand_css_markup(css_input: CssInput) -> TokenStream {
140 let css = match css_input {
141 CssInput::Literal(content) => content.value(),
142 CssInput::Tokens(tokens) => match css_tokens_to_source(tokens) {
143 Ok(css) => css,
144 Err(err) => return err.to_compile_error().into(),
145 },
146 };
147
148 if let Err(message) = validate_css(&css) {
149 return syn::Error::new(Span::call_site(), message)
150 .to_compile_error()
151 .into();
152 }
153
154 let content_lit = LitStr::new(&css, Span::call_site());
155
156 let output = quote! {
157 {
158 fn callsite_id(prefix: &str, file: &str, line: u32, col: u32) -> String {
159 let mut h: u64 = 0xcbf29ce484222325;
160 for b in file.as_bytes() {
161 h ^= *b as u64;
162 h = h.wrapping_mul(0x100000001b3);
163 }
164 for b in line.to_le_bytes() {
165 h ^= b as u64;
166 h = h.wrapping_mul(0x100000001b3);
167 }
168 for b in col.to_le_bytes() {
169 h ^= b as u64;
170 h = h.wrapping_mul(0x100000001b3);
171 }
172
173 format!("{prefix}{h:016x}")
174 }
175
176 let __id = callsite_id("mx-css-", file!(), line!(), column!());
177
178 maud::html! {
179 style data-mx-css-id=(__id) {
180 (maud::PreEscaped(#content_lit))
181 }
182 }
183 }
184 };
185
186 TokenStream::from(output)
187}
188
189fn parse_helper_ident(helper_name: LitStr, macro_name: &str) -> Result<Ident> {
190 let value = helper_name.value();
191 let parsed: TokenStream2 = value.parse().map_err(|_| {
192 syn::Error::new(
193 helper_name.span(),
194 format!("{macro_name}! helper name must be a valid Rust identifier string"),
195 )
196 })?;
197
198 let mut tokens = parsed.into_iter();
199 match (tokens.next(), tokens.next()) {
200 (Some(TokenTree::Ident(mut ident)), None) => {
201 ident.set_span(helper_name.span());
202 Ok(ident)
203 }
204 _ => Err(syn::Error::new(
205 helper_name.span(),
206 format!("{macro_name}! helper name must be a valid Rust identifier string"),
207 )),
208 }
209}
210
211fn expand_css_helper(input: CssHelperInput) -> TokenStream {
212 let component_css_helper_ident = Ident::new(COMPONENT_CSS_HELPER_FN, Span::call_site());
213 let use_default_component_helper = input.helper_name.is_none();
214 let css_fn_ident = match input.helper_name {
215 Some(name) => match parse_helper_ident(name, "css") {
216 Ok(ident) => ident,
217 Err(err) => return err.to_compile_error().into(),
218 },
219 None => Ident::new("css", Span::call_site()),
220 };
221 let css_input = match input.css {
222 CssInput::Literal(content) => quote!(#content),
223 CssInput::Tokens(tokens) => quote!(#tokens),
224 };
225
226 let output = quote! {
227 fn #css_fn_ident() -> maud::Markup {
228 ::maud_extensions::inline_css!(#css_input)
229 }
230 };
231
232 if use_default_component_helper {
233 TokenStream::from(quote! {
234 #output
235
236 #[doc(hidden)]
237 fn #component_css_helper_ident() -> maud::Markup {
238 #css_fn_ident()
239 }
240 })
241 } else {
242 TokenStream::from(output)
243 }
244}
245
246#[proc_macro]
281pub fn css(input: TokenStream) -> TokenStream {
282 let input = parse_macro_input!(input as CssHelperInput);
283 expand_css_helper(input)
284}
285
286struct RawCssInput {
287 css: LitStr,
288}
289
290impl Parse for RawCssInput {
291 fn parse(input: ParseStream) -> Result<Self> {
292 let css: LitStr = input.parse()?;
293 if !input.is_empty() {
294 return Err(input.error("raw! expects exactly one string literal argument"));
295 }
296 Ok(Self { css })
297 }
298}
299
300fn parse_raw_css_fragment(group: &Group) -> Result<String> {
301 let input = syn::parse2::<RawCssInput>(group.stream()).map_err(|_| {
302 syn::Error::new(
303 group.span(),
304 "raw! expects exactly one string literal argument",
305 )
306 })?;
307 Ok(input.css.value())
308}
309
310fn css_tokens_to_source(tokens: TokenStream2) -> Result<String> {
311 css_token_trees_to_source(tokens.into_iter().collect())
312}
313
314fn css_token_trees_to_source(tokens: Vec<TokenTree>) -> Result<String> {
315 let mut out = String::new();
316 let mut prev_word = false;
317 let mut index = 0usize;
318
319 while index < tokens.len() {
320 if let Some(raw_css) = try_parse_raw_css(&tokens, &mut index)? {
321 if prev_word {
322 out.push(' ');
323 }
324 out.push_str(&raw_css);
325 prev_word = false;
326 continue;
327 }
328
329 match &tokens[index] {
330 TokenTree::Group(group) => {
331 let (open, close) = match group.delimiter() {
332 Delimiter::Parenthesis => ('(', ')'),
333 Delimiter::Bracket => ('[', ']'),
334 Delimiter::Brace => ('{', '}'),
335 Delimiter::None => (' ', ' '),
336 };
337 let needs_space =
338 prev_word && matches!(group.delimiter(), Delimiter::Brace | Delimiter::None);
339 if needs_space {
340 out.push(' ');
341 }
342 if open != ' ' {
343 out.push(open);
344 }
345 out.push_str(&css_tokens_to_source(group.stream())?);
346 if close != ' ' {
347 out.push(close);
348 }
349 prev_word = false;
350 }
351 TokenTree::Ident(ident) => {
352 if prev_word {
353 out.push(' ');
354 }
355 out.push_str(&ident.to_string());
356 prev_word = true;
357 }
358 TokenTree::Literal(literal) => {
359 if prev_word {
360 out.push(' ');
361 }
362 out.push_str(&literal.to_string());
363 prev_word = true;
364 }
365 TokenTree::Punct(punct) => {
366 out.push(punct.as_char());
367 prev_word = false;
368 }
369 }
370
371 index += 1;
372 }
373
374 Ok(out)
375}
376
377fn try_parse_raw_css(tokens: &[TokenTree], index: &mut usize) -> Result<Option<String>> {
378 let Some(TokenTree::Ident(ident)) = tokens.get(*index) else {
379 return Ok(None);
380 };
381
382 if ident != "raw" {
383 return Ok(None);
384 }
385
386 let Some(TokenTree::Punct(punct)) = tokens.get(*index + 1) else {
387 return Ok(None);
388 };
389
390 if punct.as_char() != '!' {
391 return Ok(None);
392 }
393
394 let Some(TokenTree::Group(group)) = tokens.get(*index + 2) else {
395 return Err(syn::Error::new(
396 punct.span(),
397 "raw! expects exactly one string literal argument",
398 ));
399 };
400
401 let raw_css = parse_raw_css_fragment(group)?;
402 *index += 2;
403 Ok(Some(raw_css))
404}
405
406fn tokens_to_source(tokens: TokenStream2) -> String {
407 let mut out = String::new();
408 let mut prev_word = false;
409
410 for token in tokens {
411 match token {
412 TokenTree::Group(group) => {
413 let (open, close) = match group.delimiter() {
414 Delimiter::Parenthesis => ('(', ')'),
415 Delimiter::Bracket => ('[', ']'),
416 Delimiter::Brace => ('{', '}'),
417 Delimiter::None => (' ', ' '),
418 };
419 let needs_space =
420 prev_word && matches!(group.delimiter(), Delimiter::Brace | Delimiter::None);
421 if needs_space {
422 out.push(' ');
423 }
424 if open != ' ' {
425 out.push(open);
426 }
427 out.push_str(&tokens_to_source(group.stream()));
428 if close != ' ' {
429 out.push(close);
430 }
431 prev_word = false;
432 }
433 TokenTree::Ident(ident) => {
434 if prev_word {
435 out.push(' ');
436 }
437 out.push_str(&ident.to_string());
438 prev_word = true;
439 }
440 TokenTree::Literal(literal) => {
441 if prev_word {
442 out.push(' ');
443 }
444 out.push_str(&literal.to_string());
445 prev_word = true;
446 }
447 TokenTree::Punct(punct) => {
448 out.push(punct.as_char());
449 prev_word = false;
450 }
451 }
452 }
453
454 out
455}
456
457fn validate_css(css: &str) -> core::result::Result<(), String> {
458 let mut input = cssparser::ParserInput::new(css);
459 let mut parser = cssparser::Parser::new(&mut input);
460 loop {
461 match parser.next_including_whitespace_and_comments() {
462 Ok(_) => {}
463 Err(err) => match err.kind {
464 cssparser::BasicParseErrorKind::EndOfInput => return Ok(()),
465 _ => return Err("inline_css! could not parse CSS tokens".to_string()),
466 },
467 }
468 }
469}
470
471fn emit_script_bundles(bundles: impl IntoIterator<Item = &'static str>) -> TokenStream {
472 let bundles: Vec<LitStr> = bundles
473 .into_iter()
474 .map(|bundle| LitStr::new(bundle, Span::call_site()))
475 .collect();
476
477 quote! {
478 maud::html! {
479 #(
480 script {
481 (maud::PreEscaped(#bundles))
482 }
483 )*
484 }
485 }
486 .into()
487}
488
489fn expand_js_markup(js_input: JsInput) -> TokenStream {
490 let (content_lit, js_string) = match js_input {
491 JsInput::Literal(content) => {
492 let js_string = content.value();
493 (content, js_string)
494 }
495 JsInput::Tokens(tokens) => {
496 let js = tokens_to_source(tokens);
497 (LitStr::new(&js, Span::call_site()), js)
498 }
499 };
500 if let Err(message) = validate_js(&js_string) {
501 return syn::Error::new(Span::call_site(), message)
502 .to_compile_error()
503 .into();
504 }
505
506 let output = quote! {
507 maud::html! {
508 script {
509 (maud::PreEscaped(#content_lit))
510 }
511 }
512 };
513
514 TokenStream::from(output)
515}
516
517fn expand_js_helper(js_input: JsInput) -> TokenStream {
518 let component_js_helper_ident = Ident::new(COMPONENT_JS_HELPER_FN, Span::call_site());
519 let js_mode_attr = COMPONENT_JS_MODE_ATTR;
520 let js_ran_attr = COMPONENT_JS_RAN_ATTR;
521 let js_markup = match js_input {
522 JsInput::Literal(content) => {
523 let wrapped = format!(
524 "const __mx_script = document.currentScript;\n\
525 const __mx_root = __mx_script && __mx_script.parentElement;\n\
526 const __mx_mode = __mx_root ? __mx_root.getAttribute(\"{js_mode_attr}\") : null;\n\
527 let __mx_should_run = true;\n\
528 if (__mx_mode === \"once\" && __mx_root) {{\n\
529 if (__mx_root.hasAttribute(\"{js_ran_attr}\")) {{\n\
530 __mx_should_run = false;\n\
531 }} else {{\n\
532 __mx_root.setAttribute(\"{js_ran_attr}\", \"\");\n\
533 }}\n\
534 }}\n\
535 if (__mx_should_run) {{\n\
536 {}\n\
537 }}",
538 content.value()
539 );
540 let wrapped_lit = LitStr::new(&wrapped, Span::call_site());
541 quote! {
542 ::maud_extensions::inline_js!(#wrapped_lit)
543 }
544 }
545 JsInput::Tokens(tokens) => {
546 let js_mode_attr = LitStr::new(js_mode_attr, Span::call_site());
547 let js_ran_attr = LitStr::new(js_ran_attr, Span::call_site());
548 quote! {
549 ::maud_extensions::inline_js! {
550 const __mx_script = document.currentScript;
551 const __mx_root = __mx_script && __mx_script.parentElement;
552 const __mx_mode = __mx_root ? __mx_root.getAttribute(#js_mode_attr) : null;
553
554 let __mx_should_run = true;
555 if (__mx_mode === "once" && __mx_root) {
556 if (__mx_root.hasAttribute(#js_ran_attr)) {
557 __mx_should_run = false;
558 } else {
559 __mx_root.setAttribute(#js_ran_attr, "");
560 }
561 }
562
563 if (__mx_should_run) {
564 #tokens
565 }
566 }
567 }
568 }
569 };
570
571 let output = quote! {
572 fn js() -> maud::Markup {
573 #js_markup
574 }
575
576 #[doc(hidden)]
577 fn #component_js_helper_ident() -> maud::Markup {
578 js()
579 }
580 };
581
582 TokenStream::from(output)
583}
584
585#[proc_macro]
607pub fn js(input: TokenStream) -> TokenStream {
608 let js_input = parse_macro_input!(input as JsInput);
609 expand_js_helper(js_input)
610}
611
612#[proc_macro]
628pub fn inline_js(input: TokenStream) -> TokenStream {
629 let js_input = parse_macro_input!(input as JsInput);
630 expand_js_markup(js_input)
631}
632
633#[proc_macro]
651pub fn inline_css(input: TokenStream) -> TokenStream {
652 let css_input = parse_macro_input!(input as CssInput);
653 expand_css_markup(css_input)
654}
655
656fn component_syntax_error(span: Span) -> syn::Error {
657 syn::Error::new(span, COMPONENT_SYNTAX_ERROR)
658}
659
660fn component_directive_error(span: Span, message: &str) -> syn::Error {
661 syn::Error::new(span, message)
662}
663
664fn is_punct(token: &TokenTree, ch: char) -> bool {
665 matches!(token, TokenTree::Punct(punct) if punct.as_char() == ch)
666}
667
668fn is_ident(token: &TokenTree, expected: &str) -> bool {
669 matches!(token, TokenTree::Ident(ident) if ident == expected)
670}
671
672fn token_span(token: Option<&TokenTree>) -> Span {
673 token.map(TokenTree::span).unwrap_or_else(Span::call_site)
674}
675
676fn parse_component_js_directive(tokens: &[TokenTree]) -> Result<(ComponentJsMode, usize)> {
677 if tokens.len() < 4 {
678 return Err(component_directive_error(
679 token_span(tokens.first()),
680 "component! directive is incomplete. Use `@js-once` or `@js-always`.",
681 ));
682 }
683
684 if !is_ident(&tokens[1], "js") || !is_punct(&tokens[2], '-') {
685 return Err(component_directive_error(
686 tokens[1].span(),
687 "unknown component! directive. Supported directives are `@js-once` and `@js-always`.",
688 ));
689 }
690
691 let mode = if is_ident(&tokens[3], "once") {
692 ComponentJsMode::Once
693 } else if is_ident(&tokens[3], "always") {
694 ComponentJsMode::Always
695 } else {
696 return Err(component_directive_error(
697 tokens[3].span(),
698 "unknown component! directive. Supported directives are `@js-once` and `@js-always`.",
699 ));
700 };
701
702 let mut consumed = 4usize;
703 if matches!(tokens.get(consumed), Some(token) if is_punct(token, ';')) {
704 consumed += 1;
705 }
706 Ok((mode, consumed))
707}
708
709fn find_component_body_index(tokens: &[TokenTree]) -> Result<usize> {
710 if tokens.is_empty() {
711 return Err(component_syntax_error(Span::call_site()));
712 }
713 if !matches!(tokens.first(), Some(TokenTree::Ident(_))) {
714 return Err(component_syntax_error(token_span(tokens.first())));
715 }
716
717 if let Some(token) = tokens
718 .iter()
719 .find(|token| matches!(token, TokenTree::Punct(punct) if punct.as_char() == '@'))
720 {
721 return Err(component_directive_error(
722 token.span(),
723 "component! directives must appear before the root element.",
724 ));
725 }
726
727 let Some(body_index) = tokens.iter().position(
728 |token| matches!(token, TokenTree::Group(group) if group.delimiter() == Delimiter::Brace),
729 ) else {
730 return Err(component_syntax_error(token_span(tokens.last())));
731 };
732
733 let trailing = tokens
734 .iter()
735 .enumerate()
736 .skip(body_index + 1)
737 .find(|(_, token)| !matches!(token, TokenTree::Punct(punct) if punct.as_char() == ';'));
738 if let Some((_, token)) = trailing {
739 return Err(component_syntax_error(token.span()));
740 }
741
742 Ok(body_index)
743}
744
745#[proc_macro]
771pub fn component(input: TokenStream) -> TokenStream {
772 let component_js_helper_ident = Ident::new(COMPONENT_JS_HELPER_FN, Span::call_site());
773 let component_css_helper_ident = Ident::new(COMPONENT_CSS_HELPER_FN, Span::call_site());
774 let mut tokens: Vec<TokenTree> = TokenStream2::from(input).into_iter().collect();
775
776 while matches!(
777 tokens.last(),
778 Some(TokenTree::Punct(punct)) if punct.as_char() == ';'
779 ) {
780 tokens.pop();
781 }
782
783 if tokens.is_empty() {
784 return component_syntax_error(Span::call_site())
785 .to_compile_error()
786 .into();
787 }
788
789 let mut js_mode = ComponentJsMode::Always;
790 let mut seen_mode_directive = false;
791 let mut consumed = 0usize;
792
793 while matches!(tokens.get(consumed), Some(token) if is_punct(token, '@')) {
794 let (mode, directive_len) = match parse_component_js_directive(&tokens[consumed..]) {
795 Ok(parsed) => parsed,
796 Err(err) => return err.to_compile_error().into(),
797 };
798
799 if seen_mode_directive {
800 return component_directive_error(
801 tokens[consumed].span(),
802 "component! accepts at most one JS mode directive (`@js-once` or `@js-always`).",
803 )
804 .to_compile_error()
805 .into();
806 }
807
808 js_mode = mode;
809 seen_mode_directive = true;
810 consumed += directive_len;
811 }
812
813 if consumed > 0 {
814 tokens.drain(0..consumed);
815 }
816
817 let body_index = match find_component_body_index(&tokens) {
818 Ok(index) => index,
819 Err(err) => return err.to_compile_error().into(),
820 };
821
822 let Some(TokenTree::Group(root_group)) = tokens.get(body_index) else {
823 return component_syntax_error(token_span(tokens.last()))
824 .to_compile_error()
825 .into();
826 };
827
828 let mut injected_body = root_group.stream();
829 injected_body.extend(quote! { (#component_js_helper_ident()) (#component_css_helper_ident()) });
830 let mut updated_group = Group::new(Delimiter::Brace, injected_body);
831 updated_group.set_span(root_group.span());
832 tokens[body_index] = TokenTree::Group(updated_group);
833
834 let js_mode_lit = LitStr::new(js_mode.as_str(), Span::call_site());
835 tokens.splice(
836 body_index..body_index,
837 quote! {
838 data-mx-component=""
839 data-mx-js-mode=(#js_mode_lit)
840 },
841 );
842
843 let root_tokens: TokenStream2 = tokens.into_iter().collect();
844 quote! {
845 maud::html! {
846 #root_tokens
847 }
848 }
849 .into()
850}
851
852#[proc_macro]
867pub fn js_file(input: TokenStream) -> TokenStream {
868 let path = parse_macro_input!(input as Expr);
869 let output = quote! {
870 maud::html! {
871 script {
872 (maud::PreEscaped(include_str!(#path)))
873 }
874 }
875 };
876
877 TokenStream::from(output)
878}
879
880#[proc_macro]
895pub fn css_file(input: TokenStream) -> TokenStream {
896 let path = parse_macro_input!(input as Expr);
897 let output = quote! {
898 maud::html! {
899 style {
900 (maud::PreEscaped(include_str!(#path)))
901 }
902 }
903 };
904
905 TokenStream::from(output)
906}
907
908#[proc_macro]
922pub fn surreal_scope_inline(input: TokenStream) -> TokenStream {
923 let _ = parse_macro_input!(input as Nothing);
924 emit_script_bundles([SURREAL_JS_BUNDLE, CSS_SCOPE_INLINE_JS_BUNDLE])
925}
926
927#[proc_macro]
944pub fn signals_inline(input: TokenStream) -> TokenStream {
945 let _ = parse_macro_input!(input as Nothing);
946 emit_script_bundles([SIGNALS_CORE_JS_BUNDLE, SIGNALS_ADAPTER_JS_BUNDLE])
947}
948
949#[proc_macro]
966pub fn surreal_scope_signals_inline(input: TokenStream) -> TokenStream {
967 let _ = parse_macro_input!(input as Nothing);
968 emit_script_bundles([
969 SURREAL_JS_BUNDLE,
970 CSS_SCOPE_INLINE_JS_BUNDLE,
971 SIGNALS_CORE_JS_BUNDLE,
972 SIGNALS_ADAPTER_JS_BUNDLE,
973 ])
974}
975
976fn validate_js(js: &str) -> core::result::Result<(), String> {
977 let cm = SourceMap::default();
978 let fm = cm.new_source_file(
979 FileName::Custom("inline.js".to_string()).into(),
980 js.to_string(),
981 );
982 let input = StringInput::from(&*fm);
983 let mut parser = Parser::new(Syntax::Es(EsSyntax::default()), input, None);
984 match parser.parse_script() {
985 Ok(_) => Ok(()),
986 Err(err) => Err(format!("inline_js! could not parse JavaScript: {err:#?}")),
987 }
988}
989
990struct FontFace {
991 path: Expr,
992 family: LitStr,
993 weight: Option<LitStr>,
994 style: Option<LitStr>,
995}
996
997impl Parse for FontFace {
998 fn parse(input: ParseStream) -> syn::Result<Self> {
999 let path: Expr = input.parse()?;
1000 input.parse::<Token![,]>()?;
1001 let family: LitStr = input.parse()?;
1002
1003 let weight = if input.peek(Token![,]) {
1004 input.parse::<Token![,]>()?;
1005 if input.peek(LitStr) {
1006 Some(input.parse()?)
1007 } else {
1008 None
1009 }
1010 } else {
1011 None
1012 };
1013
1014 let style = if weight.is_some() && input.peek(Token![,]) {
1015 input.parse::<Token![,]>()?;
1016 if input.peek(LitStr) {
1017 Some(input.parse()?)
1018 } else {
1019 None
1020 }
1021 } else {
1022 None
1023 };
1024
1025 Ok(FontFace {
1026 path,
1027 family,
1028 weight,
1029 style,
1030 })
1031 }
1032}
1033
1034struct FontFaceList {
1035 fonts: Punctuated<FontFace, Token![;]>,
1036}
1037
1038impl Parse for FontFaceList {
1039 fn parse(input: ParseStream) -> syn::Result<Self> {
1040 let fonts = Punctuated::parse_terminated(input)?;
1041 Ok(FontFaceList { fonts })
1042 }
1043}
1044
1045fn expand_font_face_css(
1046 path: &Expr,
1047 family: &LitStr,
1048 weight: &LitStr,
1049 style: &LitStr,
1050) -> TokenStream2 {
1051 quote! {{
1052 fn __mx_encode_base64(bytes: &[u8]) -> String {
1053 const TABLE: &[u8; 64] =
1054 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1055
1056 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
1057 let mut chunks = bytes.chunks_exact(3);
1058 for chunk in &mut chunks {
1059 let combined =
1060 ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
1061 out.push(TABLE[((combined >> 18) & 0x3f) as usize] as char);
1062 out.push(TABLE[((combined >> 12) & 0x3f) as usize] as char);
1063 out.push(TABLE[((combined >> 6) & 0x3f) as usize] as char);
1064 out.push(TABLE[(combined & 0x3f) as usize] as char);
1065 }
1066
1067 match chunks.remainder() {
1068 [only] => {
1069 let combined = (*only as u32) << 16;
1070 out.push(TABLE[((combined >> 18) & 0x3f) as usize] as char);
1071 out.push(TABLE[((combined >> 12) & 0x3f) as usize] as char);
1072 out.push('=');
1073 out.push('=');
1074 }
1075 [first, second] => {
1076 let combined = ((*first as u32) << 16) | ((*second as u32) << 8);
1077 out.push(TABLE[((combined >> 18) & 0x3f) as usize] as char);
1078 out.push(TABLE[((combined >> 12) & 0x3f) as usize] as char);
1079 out.push(TABLE[((combined >> 6) & 0x3f) as usize] as char);
1080 out.push('=');
1081 }
1082 [] => {}
1083 _ => unreachable!("chunks_exact(3) only leaves 0, 1, or 2 trailing bytes"),
1084 }
1085
1086 out
1087 }
1088
1089 static __MX_FONT_FACE_CSS: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
1090
1091 __MX_FONT_FACE_CSS
1092 .get_or_init(|| {
1093 let __mx_bytes = include_bytes!(#path);
1094 let __mx_path = (#path).to_ascii_lowercase();
1095 let (__mx_font_type, __mx_format) = if __mx_path.ends_with(".woff2") {
1096 ("woff2", "woff2")
1097 } else if __mx_path.ends_with(".woff") {
1098 ("woff", "woff")
1099 } else if __mx_path.ends_with(".otf") {
1100 ("opentype", "opentype")
1101 } else {
1102 ("truetype", "truetype")
1103 };
1104 let __mx_base64 = __mx_encode_base64(__mx_bytes);
1105 format!(
1106 "@font-face {{\n font-family: '{}';\n src: url('data:font/{};base64,{}') format('{}');\n font-weight: {};\n font-style: {};\n}}",
1107 #family,
1108 __mx_font_type,
1109 __mx_base64,
1110 __mx_format,
1111 #weight,
1112 #style
1113 )
1114 })
1115 .clone()
1116 }}
1117}
1118
1119#[proc_macro]
1142pub fn font_face(input: TokenStream) -> TokenStream {
1143 let font = parse_macro_input!(input as FontFace);
1144
1145 let weight = font
1146 .weight
1147 .unwrap_or_else(|| LitStr::new("normal", Span::call_site()));
1148 let style = font
1149 .style
1150 .unwrap_or_else(|| LitStr::new("normal", Span::call_site()));
1151 let css = expand_font_face_css(&font.path, &font.family, &weight, &style);
1152
1153 quote! {{
1154 maud::PreEscaped(#css)
1155 }}
1156 .into()
1157}
1158
1159#[proc_macro]
1182pub fn font_faces(input: TokenStream) -> TokenStream {
1183 let fonts = parse_macro_input!(input as FontFaceList);
1184
1185 let font_faces = fonts.fonts.iter().map(|font| {
1186 let weight = font
1187 .weight
1188 .as_ref()
1189 .cloned()
1190 .unwrap_or_else(|| LitStr::new("normal", Span::call_site()));
1191 let style = font
1192 .style
1193 .as_ref()
1194 .cloned()
1195 .unwrap_or_else(|| LitStr::new("normal", Span::call_site()));
1196 let css = expand_font_face_css(&font.path, &font.family, &weight, &style);
1197
1198 quote! {
1199 css.push_str(&#css);
1200 }
1201 });
1202
1203 quote! {{
1204 let mut css = String::new();
1205 #(#font_faces)*
1206 maud::PreEscaped(css)
1207 }}
1208 .into()
1209}
1210
1211#[derive(Clone)]
1212enum BuilderFieldKind {
1213 Required,
1214 Optional { inner: Type },
1215 Repeated { inner: Type },
1216 Defaulted,
1217}
1218
1219#[derive(Clone, Default)]
1220struct SlotAttr {
1221 is_slot: bool,
1222 is_default: bool,
1223}
1224
1225#[derive(Clone, Default)]
1226struct BuilderAttr {
1227 use_default: bool,
1228 each_method: Option<Ident>,
1229}
1230
1231#[derive(Clone)]
1232enum BuilderInputMode {
1233 Direct(Box<Type>),
1234 RenderToMarkup,
1235}
1236
1237#[derive(Clone)]
1238struct BuilderField {
1239 ident: Ident,
1240 ty: Type,
1241 kind: BuilderFieldKind,
1242 slot: SlotAttr,
1243 builder: BuilderAttr,
1244 setter_input: BuilderInputMode,
1245 repeated_item_input: Option<BuilderInputMode>,
1246 state_ident: Option<Ident>,
1247}
1248
1249struct BuilderExpansionCtx<'a, 'b> {
1250 builder_ident: &'a Ident,
1251 existing_args: &'a [TokenStream2],
1252 builder_generics: &'a Generics,
1253 built_ident: &'a Ident,
1254 built_field_ident: &'a Ident,
1255 fields: &'a [BuilderField],
1256 required_fields: &'a [&'b BuilderField],
1257}
1258
1259#[proc_macro_derive(ComponentBuilder, attributes(builder, slot))]
1305pub fn component_builder(input: TokenStream) -> TokenStream {
1306 let input = parse_macro_input!(input as DeriveInput);
1307 expand_component_builder(input)
1308}
1309
1310fn expand_component_builder(input: DeriveInput) -> TokenStream {
1311 let ident = input.ident;
1312 let vis = input.vis;
1313 let generics = input.generics;
1314
1315 let Data::Struct(data_struct) = input.data else {
1316 return syn::Error::new(
1317 ident.span(),
1318 "ComponentBuilder only supports structs with named fields.",
1319 )
1320 .to_compile_error()
1321 .into();
1322 };
1323
1324 let Fields::Named(fields_named) = data_struct.fields else {
1325 return syn::Error::new(
1326 ident.span(),
1327 "ComponentBuilder only supports structs with named fields.",
1328 )
1329 .to_compile_error()
1330 .into();
1331 };
1332
1333 let parsed_fields = match fields_named
1334 .named
1335 .iter()
1336 .enumerate()
1337 .map(|(index, field)| parse_builder_field(index, field))
1338 .collect::<syn::Result<Vec<_>>>()
1339 {
1340 Ok(fields) => fields,
1341 Err(err) => return err.to_compile_error().into(),
1342 };
1343
1344 if let Err(err) = validate_builder_fields(&parsed_fields) {
1345 return err.to_compile_error().into();
1346 }
1347
1348 let builder_ident = format_ident!("{ident}Builder");
1349 let existing_args = generic_args_from_generics(&generics);
1350 let component_ty = component_type_tokens(&ident, &existing_args);
1351 let built_ident = format_ident!("__Built");
1352 let built_field_ident = format_ident!("__maud_extensions_built");
1353
1354 let required_fields: Vec<&BuilderField> = parsed_fields
1355 .iter()
1356 .filter(|field| matches!(field.kind, BuilderFieldKind::Required))
1357 .collect();
1358
1359 let mut builder_generics = generics.clone();
1360 for field in &required_fields {
1361 let state_ident = field
1362 .state_ident
1363 .as_ref()
1364 .expect("required fields always carry a state ident");
1365 builder_generics
1366 .params
1367 .push(parse_quote!(const #state_ident: bool));
1368 }
1369 builder_generics
1370 .params
1371 .push(parse_quote!(#built_ident = #component_ty));
1372
1373 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1374 let (_builder_impl_generics, _builder_ty_generics, builder_where_clause) =
1375 builder_generics.split_for_impl();
1376
1377 let new_builder_ty = builder_type_tokens(
1378 &builder_ident,
1379 &existing_args,
1380 required_fields.iter().map(|_| quote!(false)).collect(),
1381 None,
1382 );
1383
1384 let builder_struct_fields = parsed_fields.iter().map(|field| {
1385 let ident = &field.ident;
1386 let storage_ty = builder_storage_ty(field);
1387 quote! { #ident: #storage_ty }
1388 });
1389 let builder_marker_field = quote! {
1390 #built_field_ident: ::core::marker::PhantomData<fn() -> #built_ident>
1391 };
1392
1393 let builder_init_fields = parsed_fields.iter().map(|field| {
1394 let ident = &field.ident;
1395 let init = builder_init_expr(field);
1396 quote! { #ident: #init }
1397 });
1398 let builder_marker_init = quote! {
1399 #built_field_ident: ::core::marker::PhantomData
1400 };
1401
1402 let component_new_impl = quote! {
1403 impl #impl_generics #ident #ty_generics #where_clause {
1404 #[must_use]
1405 pub fn new() -> #new_builder_ty {
1406 #builder_ident {
1407 #(#builder_init_fields,)*
1408 #builder_marker_init
1409 }
1410 }
1411
1412 #[must_use]
1413 pub fn builder() -> #new_builder_ty {
1414 Self::new()
1415 }
1416 }
1417 };
1418
1419 let setters = parsed_fields
1420 .iter()
1421 .map(|field| {
1422 let ctx = BuilderExpansionCtx {
1423 builder_ident: &builder_ident,
1424 existing_args: &existing_args,
1425 builder_generics: &builder_generics,
1426 built_ident: &built_ident,
1427 built_field_ident: &built_field_ident,
1428 fields: &parsed_fields,
1429 required_fields: &required_fields,
1430 };
1431 let method = expand_builder_field_setter(&ctx, field);
1432 let maybe = expand_builder_optional_setter(&ctx, field);
1433 let each = expand_builder_each_setter(&ctx, field);
1434
1435 quote! {
1436 #method
1437 #maybe
1438 #each
1439 }
1440 })
1441 .collect::<Vec<_>>();
1442
1443 let build_ctx = BuilderExpansionCtx {
1444 builder_ident: &builder_ident,
1445 existing_args: &existing_args,
1446 builder_generics: &builder_generics,
1447 built_ident: &built_ident,
1448 built_field_ident: &built_field_ident,
1449 fields: &parsed_fields,
1450 required_fields: &required_fields,
1451 };
1452 let build_impl = expand_builder_build_impl(&build_ctx, &ident, &generics, &component_ty);
1453
1454 let output = quote! {
1455 #vis struct #builder_ident #builder_generics #builder_where_clause {
1456 #(#builder_struct_fields,)*
1457 #builder_marker_field
1458 }
1459
1460 #component_new_impl
1461 #(#setters)*
1462 #build_impl
1463 };
1464
1465 output.into()
1466}
1467
1468fn parse_builder_field(field_index: usize, field: &syn::Field) -> syn::Result<BuilderField> {
1469 let ident = field
1470 .ident
1471 .clone()
1472 .ok_or_else(|| syn::Error::new(field.span(), "ComponentBuilder requires named fields."))?;
1473
1474 let slot = parse_slot_attr(&field.attrs)?;
1475 let builder = parse_builder_attr(&field.attrs)?;
1476 let kind = classify_builder_field(&field.ty, builder.use_default);
1477 let setter_input = match &kind {
1478 BuilderFieldKind::Repeated { .. } => BuilderInputMode::Direct(Box::new(field.ty.clone())),
1479 _ => setter_input_mode(&field.ty, &kind),
1480 };
1481 let repeated_item_input = repeated_item_input_mode(&kind);
1482 let state_ident = matches!(kind, BuilderFieldKind::Required)
1483 .then(|| format_ident!("__MAUD_EXTENSIONS_REQUIRED_FIELD_{field_index}_SET"));
1484
1485 Ok(BuilderField {
1486 ident,
1487 ty: field.ty.clone(),
1488 kind,
1489 slot,
1490 builder,
1491 setter_input,
1492 repeated_item_input,
1493 state_ident,
1494 })
1495}
1496
1497fn parse_slot_attr(attrs: &[syn::Attribute]) -> syn::Result<SlotAttr> {
1498 let mut slot = SlotAttr::default();
1499
1500 for attr in attrs {
1501 if !attr.path().is_ident("slot") {
1502 continue;
1503 }
1504
1505 slot.is_slot = true;
1506 if matches!(&attr.meta, syn::Meta::Path(_)) {
1507 continue;
1508 }
1509
1510 attr.parse_nested_meta(|meta| {
1511 if meta.path.is_ident("default") {
1512 slot.is_default = true;
1513 return Ok(());
1514 }
1515
1516 if meta.path.is_ident("optional") {
1517 return Ok(());
1518 }
1519
1520 Err(meta.error(
1521 "unsupported slot attribute. Supported forms are `#[slot]` and `#[slot(default)]`.",
1522 ))
1523 })?;
1524 }
1525
1526 Ok(slot)
1527}
1528
1529fn parse_builder_attr(attrs: &[syn::Attribute]) -> syn::Result<BuilderAttr> {
1530 let mut builder = BuilderAttr::default();
1531
1532 for attr in attrs {
1533 if !attr.path().is_ident("builder") {
1534 continue;
1535 }
1536
1537 attr.parse_nested_meta(|meta| {
1538 if meta.path.is_ident("default") {
1539 builder.use_default = true;
1540 return Ok(());
1541 }
1542
1543 if meta.path.is_ident("each") {
1544 let value = meta.value()?;
1545 let lit: LitStr = value.parse()?;
1546 builder.each_method = Some(Ident::new(&lit.value(), lit.span()));
1547 return Ok(());
1548 }
1549
1550 Err(meta.error(
1551 "unsupported builder attribute. Supported forms are `#[builder(default)]` and `#[builder(each = \"item\")]`.",
1552 ))
1553 })?;
1554 }
1555
1556 Ok(builder)
1557}
1558
1559fn classify_builder_field(ty: &Type, use_default: bool) -> BuilderFieldKind {
1560 if let Some(inner) = option_inner_ty(ty) {
1561 return BuilderFieldKind::Optional { inner };
1562 }
1563
1564 if let Some(inner) = vec_inner_ty(ty) {
1565 return BuilderFieldKind::Repeated { inner };
1566 }
1567
1568 if use_default {
1569 return BuilderFieldKind::Defaulted;
1570 }
1571
1572 BuilderFieldKind::Required
1573}
1574
1575fn validate_builder_fields(fields: &[BuilderField]) -> syn::Result<()> {
1576 let default_slots = fields.iter().filter(|field| field.slot.is_default).count();
1577 if default_slots > 1 {
1578 let duplicate = fields
1579 .iter()
1580 .find(|field| field.slot.is_default)
1581 .expect("count verified");
1582 return Err(syn::Error::new(
1583 duplicate.ident.span(),
1584 "ComponentBuilder allows at most one `#[slot(default)]` field.",
1585 ));
1586 }
1587
1588 for field in fields {
1589 if field.builder.each_method.is_some()
1590 && !matches!(field.kind, BuilderFieldKind::Repeated { .. })
1591 {
1592 return Err(syn::Error::new(
1593 field.ident.span(),
1594 "`#[builder(each = \"...\")]` only applies to `Vec<T>` fields.",
1595 ));
1596 }
1597
1598 if let Some(each) = &field.builder.each_method {
1599 if each == &field.ident {
1600 return Err(syn::Error::new(
1601 each.span(),
1602 "`#[builder(each = \"...\")]` must use a method name different from the field name.",
1603 ));
1604 }
1605 }
1606 }
1607
1608 let mut method_names = std::collections::BTreeSet::new();
1609 method_names.insert("build".to_string());
1610 method_names.insert("render".to_string());
1611
1612 for field in fields {
1613 let field_method = field.ident.unraw().to_string();
1614 if !method_names.insert(field_method.clone()) {
1615 return Err(syn::Error::new(
1616 field.ident.span(),
1617 format!("duplicate generated builder method `{field_method}`."),
1618 ));
1619 }
1620
1621 if let Some(maybe) = optional_setter_ident(field) {
1622 let maybe_method = maybe.unraw().to_string();
1623 if !method_names.insert(maybe_method.clone()) {
1624 return Err(syn::Error::new(
1625 maybe.span(),
1626 format!("duplicate generated builder method `{maybe_method}`."),
1627 ));
1628 }
1629 }
1630
1631 if let Some(each) = &field.builder.each_method {
1632 let each_method = each.unraw().to_string();
1633 if !method_names.insert(each_method.clone()) {
1634 return Err(syn::Error::new(
1635 each.span(),
1636 format!("duplicate generated builder method `{each_method}`."),
1637 ));
1638 }
1639 }
1640 }
1641
1642 Ok(())
1643}
1644
1645fn option_inner_ty(ty: &Type) -> Option<Type> {
1646 generic_inner_ty(
1647 ty,
1648 &[
1649 &["Option"],
1650 &["std", "option", "Option"],
1651 &["core", "option", "Option"],
1652 ],
1653 )
1654}
1655
1656fn vec_inner_ty(ty: &Type) -> Option<Type> {
1657 generic_inner_ty(
1658 ty,
1659 &[&["Vec"], &["std", "vec", "Vec"], &["alloc", "vec", "Vec"]],
1660 )
1661}
1662
1663fn generic_inner_ty(ty: &Type, accepted_paths: &[&[&str]]) -> Option<Type> {
1664 let Type::Path(TypePath { qself: None, path }) = ty else {
1665 return None;
1666 };
1667
1668 if !path_matches_any(path, accepted_paths) {
1669 return None;
1670 }
1671
1672 let segment = path.segments.last()?;
1673 let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
1674 return None;
1675 };
1676
1677 if args.args.len() != 1 {
1678 return None;
1679 }
1680
1681 let syn::GenericArgument::Type(inner) = args.args.first()? else {
1682 return None;
1683 };
1684
1685 Some(inner.clone())
1686}
1687
1688fn is_markup_ty(ty: &Type) -> bool {
1689 let Type::Path(TypePath { qself: None, path }) = ty else {
1690 return false;
1691 };
1692
1693 path_matches_any(path, &[&["Markup"], &["maud", "Markup"]])
1694}
1695
1696fn path_matches_any(path: &syn::Path, accepted_paths: &[&[&str]]) -> bool {
1697 accepted_paths
1698 .iter()
1699 .any(|segments| path_matches_segments(path, segments))
1700}
1701
1702fn path_matches_segments(path: &syn::Path, expected_segments: &[&str]) -> bool {
1703 if path.segments.len() != expected_segments.len() {
1704 return false;
1705 }
1706
1707 path.segments
1708 .iter()
1709 .zip(expected_segments.iter())
1710 .all(|(segment, expected)| segment.ident == expected)
1711}
1712
1713fn builder_storage_ty(field: &BuilderField) -> TokenStream2 {
1714 match field.kind {
1715 BuilderFieldKind::Required => {
1716 let ty = &field.ty;
1717 quote!(::core::option::Option<#ty>)
1718 }
1719 _ => {
1720 let ty = &field.ty;
1721 quote!(#ty)
1722 }
1723 }
1724}
1725
1726fn builder_init_expr(field: &BuilderField) -> TokenStream2 {
1727 match field.kind {
1728 BuilderFieldKind::Required => quote!(::core::option::Option::None),
1729 BuilderFieldKind::Optional { .. } => quote!(::core::option::Option::None),
1730 BuilderFieldKind::Repeated { .. } => quote!(::std::vec::Vec::new()),
1731 BuilderFieldKind::Defaulted => quote!(::core::default::Default::default()),
1732 }
1733}
1734
1735fn setter_input_mode(ty: &Type, kind: &BuilderFieldKind) -> BuilderInputMode {
1736 match kind {
1737 BuilderFieldKind::Required | BuilderFieldKind::Defaulted => {
1738 if is_markup_ty(ty) {
1739 BuilderInputMode::RenderToMarkup
1740 } else {
1741 BuilderInputMode::Direct(Box::new(ty.clone()))
1742 }
1743 }
1744 BuilderFieldKind::Optional { inner } => {
1745 if is_markup_ty(inner) {
1746 BuilderInputMode::RenderToMarkup
1747 } else {
1748 BuilderInputMode::Direct(Box::new(inner.clone()))
1749 }
1750 }
1751 BuilderFieldKind::Repeated { .. } => {
1752 unreachable!("repeated fields use repeated_item_input_mode")
1753 }
1754 }
1755}
1756
1757fn repeated_item_input_mode(kind: &BuilderFieldKind) -> Option<BuilderInputMode> {
1758 let BuilderFieldKind::Repeated { inner } = kind else {
1759 return None;
1760 };
1761
1762 Some(if is_markup_ty(inner) {
1763 BuilderInputMode::RenderToMarkup
1764 } else {
1765 BuilderInputMode::Direct(Box::new(inner.clone()))
1766 })
1767}
1768
1769fn generic_args_from_generics(generics: &Generics) -> Vec<TokenStream2> {
1770 generics
1771 .params
1772 .iter()
1773 .map(|param| match param {
1774 GenericParam::Type(param) => {
1775 let ident = ¶m.ident;
1776 quote!(#ident)
1777 }
1778 GenericParam::Lifetime(param) => {
1779 let lifetime = ¶m.lifetime;
1780 quote!(#lifetime)
1781 }
1782 GenericParam::Const(param) => {
1783 let ident = ¶m.ident;
1784 quote!(#ident)
1785 }
1786 })
1787 .collect()
1788}
1789
1790fn builder_type_tokens(
1791 builder_ident: &Ident,
1792 existing_args: &[TokenStream2],
1793 state_args: Vec<TokenStream2>,
1794 built_arg: Option<TokenStream2>,
1795) -> TokenStream2 {
1796 let mut all_args = existing_args.to_vec();
1797 all_args.extend(state_args);
1798 if let Some(built_arg) = built_arg {
1799 all_args.push(built_arg);
1800 }
1801
1802 if all_args.is_empty() {
1803 quote!(#builder_ident)
1804 } else {
1805 quote!(#builder_ident < #(#all_args),* >)
1806 }
1807}
1808
1809fn optional_setter_ident(field: &BuilderField) -> Option<Ident> {
1810 matches!(field.kind, BuilderFieldKind::Optional { .. })
1811 .then(|| format_ident!("maybe_{}", field.ident.unraw(), span = field.ident.span()))
1812}
1813
1814fn current_state_args(ctx: &BuilderExpansionCtx<'_, '_>) -> Vec<TokenStream2> {
1815 ctx.required_fields
1816 .iter()
1817 .map(|required| {
1818 let state_ident = required
1819 .state_ident
1820 .as_ref()
1821 .expect("required field state ident");
1822 quote!(#state_ident)
1823 })
1824 .collect()
1825}
1826
1827fn component_type_tokens(component_ident: &Ident, existing_args: &[TokenStream2]) -> TokenStream2 {
1828 if existing_args.is_empty() {
1829 quote!(#component_ident)
1830 } else {
1831 quote!(#component_ident < #(#existing_args),* >)
1832 }
1833}
1834
1835fn expand_builder_field_setter(
1836 ctx: &BuilderExpansionCtx<'_, '_>,
1837 field: &BuilderField,
1838) -> TokenStream2 {
1839 let (impl_generics, _ty_generics, where_clause) = ctx.builder_generics.split_for_impl();
1840 let method_ident = &field.ident;
1841 let builder_ident = ctx.builder_ident;
1842 let built_ident = ctx.built_ident;
1843 let built_field_ident = ctx.built_field_ident;
1844 let current_state_args = current_state_args(ctx);
1845
1846 let return_state_args = ctx
1847 .required_fields
1848 .iter()
1849 .map(|required| {
1850 if required.ident == field.ident {
1851 quote!(true)
1852 } else {
1853 let state_ident = required
1854 .state_ident
1855 .as_ref()
1856 .expect("required field state ident");
1857 quote!(#state_ident)
1858 }
1859 })
1860 .collect::<Vec<_>>();
1861
1862 let current_ty = builder_type_tokens(
1863 builder_ident,
1864 ctx.existing_args,
1865 current_state_args,
1866 Some(quote!(#built_ident)),
1867 );
1868 let return_ty = builder_type_tokens(
1869 builder_ident,
1870 ctx.existing_args,
1871 return_state_args,
1872 Some(quote!(#built_ident)),
1873 );
1874 let rebuild_fields = ctx.fields.iter().map(|other| {
1875 let ident = &other.ident;
1876 if ident == &field.ident {
1877 let value_expr = setter_value_expr(field);
1878 quote!(#ident: #value_expr)
1879 } else {
1880 quote!(#ident: self.#ident)
1881 }
1882 });
1883
1884 let (arg_tokens, setter_prelude) = setter_arg_tokens(field);
1885
1886 quote! {
1887 impl #impl_generics #current_ty #where_clause {
1888 #[must_use]
1889 pub fn #method_ident(self, #arg_tokens) -> #return_ty {
1890 #setter_prelude
1891 #builder_ident {
1892 #(#rebuild_fields,)*
1893 #built_field_ident: ::core::marker::PhantomData
1894 }
1895 }
1896 }
1897 }
1898}
1899
1900fn expand_builder_optional_setter(
1901 ctx: &BuilderExpansionCtx<'_, '_>,
1902 field: &BuilderField,
1903) -> TokenStream2 {
1904 let Some(method_ident) = optional_setter_ident(field) else {
1905 return TokenStream2::new();
1906 };
1907 let BuilderFieldKind::Optional { inner } = &field.kind else {
1908 return TokenStream2::new();
1909 };
1910
1911 let (impl_generics, _ty_generics, where_clause) = ctx.builder_generics.split_for_impl();
1912 let builder_ident = ctx.builder_ident;
1913 let built_ident = ctx.built_ident;
1914 let built_field_ident = ctx.built_field_ident;
1915 let current_state_args = current_state_args(ctx);
1916
1917 let current_ty = builder_type_tokens(
1918 builder_ident,
1919 ctx.existing_args,
1920 current_state_args,
1921 Some(quote!(#built_ident)),
1922 );
1923 let rebuild_fields = ctx.fields.iter().map(|other| {
1924 let ident = &other.ident;
1925 if ident == &field.ident {
1926 quote!(#ident: value)
1927 } else {
1928 quote!(#ident: self.#ident)
1929 }
1930 });
1931
1932 quote! {
1933 impl #impl_generics #current_ty #where_clause {
1934 #[must_use]
1935 pub fn #method_ident(self, value: ::core::option::Option<#inner>) -> Self {
1936 #builder_ident {
1937 #(#rebuild_fields,)*
1938 #built_field_ident: ::core::marker::PhantomData
1939 }
1940 }
1941 }
1942 }
1943}
1944
1945fn expand_builder_each_setter(
1946 ctx: &BuilderExpansionCtx<'_, '_>,
1947 field: &BuilderField,
1948) -> TokenStream2 {
1949 let Some(each_ident) = &field.builder.each_method else {
1950 return TokenStream2::new();
1951 };
1952
1953 let (impl_generics, _ty_generics, where_clause) = ctx.builder_generics.split_for_impl();
1954 let builder_ident = ctx.builder_ident;
1955 let built_ident = ctx.built_ident;
1956 let built_field_ident = ctx.built_field_ident;
1957 let current_state_args = current_state_args(ctx);
1958
1959 let current_ty = builder_type_tokens(
1960 builder_ident,
1961 ctx.existing_args,
1962 current_state_args,
1963 Some(quote!(#built_ident)),
1964 );
1965 let repeated_field_ident = &field.ident;
1966 let rebuild_fields = ctx.fields.iter().map(|other| {
1967 let ident = &other.ident;
1968 if ident == repeated_field_ident {
1969 quote!(#ident: #repeated_field_ident)
1970 } else {
1971 quote!(#ident: self.#ident)
1972 }
1973 });
1974
1975 let (arg_tokens, push_expr) = each_setter_arg_tokens(field);
1976
1977 quote! {
1978 impl #impl_generics #current_ty #where_clause {
1979 #[must_use]
1980 pub fn #each_ident(self, #arg_tokens) -> Self {
1981 let mut #repeated_field_ident = self.#repeated_field_ident;
1982 #push_expr
1983 #builder_ident {
1984 #(#rebuild_fields,)*
1985 #built_field_ident: ::core::marker::PhantomData
1986 }
1987 }
1988 }
1989 }
1990}
1991
1992fn setter_arg_tokens(field: &BuilderField) -> (TokenStream2, TokenStream2) {
1993 match &field.kind {
1994 BuilderFieldKind::Repeated { .. } => match field
1995 .repeated_item_input
1996 .as_ref()
1997 .expect("repeated fields always expose item input")
1998 {
1999 BuilderInputMode::Direct(inner) => (
2000 quote!(values: impl ::core::iter::IntoIterator<Item = #inner>),
2001 quote!(),
2002 ),
2003 BuilderInputMode::RenderToMarkup => (
2004 quote!(values: impl ::core::iter::IntoIterator<Item = impl ::maud::Render>),
2005 quote!(),
2006 ),
2007 },
2008 _ => match &field.setter_input {
2009 BuilderInputMode::Direct(ty) => (quote!(value: #ty), quote!()),
2010 BuilderInputMode::RenderToMarkup => (quote!(value: impl ::maud::Render), quote!()),
2011 },
2012 }
2013}
2014
2015fn setter_value_expr(field: &BuilderField) -> TokenStream2 {
2016 match &field.kind {
2017 BuilderFieldKind::Required => match &field.setter_input {
2018 BuilderInputMode::Direct(_) => quote!(::core::option::Option::Some(value)),
2019 BuilderInputMode::RenderToMarkup => {
2020 quote!(::core::option::Option::Some(::maud::Render::render(&value)))
2021 }
2022 },
2023 BuilderFieldKind::Optional { .. } => match &field.setter_input {
2024 BuilderInputMode::Direct(_) => quote!(::core::option::Option::Some(value)),
2025 BuilderInputMode::RenderToMarkup => {
2026 quote!(::core::option::Option::Some(::maud::Render::render(&value)))
2027 }
2028 },
2029 BuilderFieldKind::Repeated { .. } => match field
2030 .repeated_item_input
2031 .as_ref()
2032 .expect("repeated fields always expose item input")
2033 {
2034 BuilderInputMode::Direct(_) => quote!(values.into_iter().collect()),
2035 BuilderInputMode::RenderToMarkup => {
2036 quote!(
2037 values
2038 .into_iter()
2039 .map(|value| ::maud::Render::render(&value))
2040 .collect()
2041 )
2042 }
2043 },
2044 BuilderFieldKind::Defaulted => match &field.setter_input {
2045 BuilderInputMode::Direct(_) => quote!(value),
2046 BuilderInputMode::RenderToMarkup => quote!(::maud::Render::render(&value)),
2047 },
2048 }
2049}
2050
2051fn each_setter_arg_tokens(field: &BuilderField) -> (TokenStream2, TokenStream2) {
2052 let repeated_field_ident = &field.ident;
2053 match field
2054 .repeated_item_input
2055 .as_ref()
2056 .expect("each setters only exist for repeated fields")
2057 {
2058 BuilderInputMode::Direct(inner) => (
2059 quote!(value: #inner),
2060 quote!(#repeated_field_ident.push(value);),
2061 ),
2062 BuilderInputMode::RenderToMarkup => (
2063 quote!(value: impl ::maud::Render),
2064 quote!(#repeated_field_ident.push(::maud::Render::render(&value));),
2065 ),
2066 }
2067}
2068
2069fn expand_builder_build_impl(
2070 ctx: &BuilderExpansionCtx<'_, '_>,
2071 component_ident: &Ident,
2072 generics: &Generics,
2073 component_ty: &TokenStream2,
2074) -> TokenStream2 {
2075 let builder_ident = ctx.builder_ident;
2076 let existing_args = ctx.existing_args;
2077 let built_ident = ctx.built_ident;
2078 let built_field_ident = ctx.built_field_ident;
2079 let fields = ctx.fields;
2080 let required_fields = ctx.required_fields;
2081 let complete_builder_ty = builder_type_tokens(
2082 builder_ident,
2083 existing_args,
2084 required_fields.iter().map(|_| quote!(true)).collect(),
2085 Some(quote!(#built_ident)),
2086 );
2087 let complete_component_builder_ty = builder_type_tokens(
2088 builder_ident,
2089 existing_args,
2090 required_fields.iter().map(|_| quote!(true)).collect(),
2091 None,
2092 );
2093
2094 let build_fields = fields.iter().map(|field| {
2095 let ident = &field.ident;
2096 match field.kind {
2097 BuilderFieldKind::Required => {
2098 let field_name = ident.to_string();
2099 quote! {
2100 #ident: #ident.expect(concat!(
2101 "ComponentBuilder state bug: missing required field `",
2102 #field_name,
2103 "` at build time."
2104 ))
2105 }
2106 }
2107 _ => quote!(#ident: #ident),
2108 }
2109 });
2110
2111 let destructure_fields = fields.iter().map(|field| &field.ident);
2112 let mut builder_generics = generics.clone();
2113 builder_generics
2114 .params
2115 .push(parse_quote!(#built_ident = #component_ty));
2116 let (builder_impl_generics, _builder_ty_generics, builder_where_clause) =
2117 builder_generics.split_for_impl();
2118 let (impl_generics, _ty_generics, where_clause) = generics.split_for_impl();
2119
2120 quote! {
2121 impl #builder_impl_generics #complete_builder_ty #builder_where_clause {
2122 #[must_use]
2123 pub fn build(self) -> #built_ident
2124 where
2125 #component_ty: ::core::convert::Into<#built_ident>,
2126 {
2127 let Self {
2128 #(#destructure_fields,)*
2129 #built_field_ident: _
2130 } = self;
2131 let component = #component_ident {
2132 #(#build_fields),*
2133 };
2134 ::core::convert::Into::into(component)
2135 }
2136
2137 #[must_use]
2138 pub fn render(self) -> ::maud::Markup
2139 where
2140 #component_ty: ::core::convert::Into<#built_ident>,
2141 #built_ident: ::maud::Render,
2142 {
2143 let component = self.build();
2144 ::maud::Render::render(&component)
2145 }
2146 }
2147
2148 impl #impl_generics ::core::convert::From<#complete_component_builder_ty> for #component_ty #where_clause {
2149 fn from(builder: #complete_component_builder_ty) -> Self {
2150 builder.build()
2151 }
2152 }
2153 }
2154}