pulldown_html_ext_derive/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};

#[proc_macro_attribute]
pub fn html_writer(attr: TokenStream, input: TokenStream) -> TokenStream {
    let skip_docs = attr.to_string().contains("skip_docs");
    let input = parse_macro_input!(input as DeriveInput);

    match process_html_writer(&input, skip_docs) {
        Ok(output) => output.into(),
        Err(err) => err.to_compile_error().into(),
    }
}

fn process_html_writer(
    input: &DeriveInput,
    skip_docs: bool,
) -> syn::Result<proc_macro2::TokenStream> {
    let name = &input.ident;
    let _generics = &input.generics;

    // Validate the input is a struct
    let fields = match &input.data {
        Data::Struct(data_struct) => match &data_struct.fields {
            Fields::Named(fields) => fields,
            _ => {
                return Err(syn::Error::new_spanned(
                    &input.ident,
                    "struct must have named fields",
                ))
            }
        },
        Data::Enum(_) => {
            return Err(syn::Error::new_spanned(
                &input.ident,
                "html_writer can only be applied to structs, not enums",
            ))
        }
        Data::Union(_) => {
            return Err(syn::Error::new_spanned(
                &input.ident,
                "html_writer can only be applied to structs, not unions",
            ))
        }
    };

    // Validate base field exists and has correct type
    let base_field = fields
        .named
        .iter()
        .find(|f| f.ident.as_ref().map_or(false, |i| i == "base"))
        .ok_or_else(|| syn::Error::new_spanned(fields, "struct must have a field named 'base'"))?;

    // Check base field type is HtmlWriterBase<W>
    let base_type = &base_field.ty;
    let is_valid_base_type = quote!(#base_type).to_string().contains("HtmlWriterBase");
    if !is_valid_base_type {
        return Err(syn::Error::new_spanned(
            base_type,
            "base field must be of type HtmlWriterBase<W>",
        ));
    }

    // Generate implementation
    let docs = if !skip_docs {
        quote! {
            #[doc = "An HTML writer implementation."]
            #[doc = "This type implements methods for writing HTML elements."]
        }
    } else {
        quote! {}
    };

    let expanded = quote! {
        #docs
        #input

        impl<W: StrWrite> HtmlWriter<W> for #name<W> {}
    };

    Ok(expanded)
}