example_html_highlight_macro/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Ident, Span};
3use quote::quote;
4use syn::parse::{Parse, ParseStream};
5use syn::spanned::Spanned;
6use syn::{Expr, ExprArray, ExprLit, Lit, Token};
7use syntect::highlighting::{Color, ThemeSet};
8use syntect::html::highlighted_html_for_string;
9use syntect::parsing::SyntaxSet;
10
11struct ExampleHtmlArgs {
12    themes: Option<Vec<String>>,
13}
14
15impl Parse for ExampleHtmlArgs {
16    fn parse(input: ParseStream) -> syn::Result<Self> {
17        let mut themes = None;
18
19        while !input.is_empty() {
20            let ident: Ident = input.parse()?;
21            match ident.to_string().as_str() {
22                "themes" => {
23                    let mut out_themes = vec![];
24                    input.parse::<Token![=]>()?;
25                    let themes_ = input.parse::<ExprArray>()?;
26
27                    for theme in themes_.elems {
28                        match theme {
29                            Expr::Lit(ExprLit { lit, .. }) => match lit {
30                                Lit::Str(str) => out_themes.push(str.value()),
31                                _ => {
32                                    return Err(syn::Error::new(
33                                        Span::call_site(),
34                                        "expected a string literal",
35                                    ))
36                                }
37                            },
38                            _ => {
39                                return Err(syn::Error::new(
40                                    Span::call_site(),
41                                    "expected a string literal",
42                                ))
43                            }
44                        }
45                    }
46
47                    themes = Some(out_themes);
48                }
49                _ => {
50                    return Err(syn::Error::new(
51                        ident.span(),
52                        format!("unexpected argument: {}", ident),
53                    ))
54                }
55            }
56        }
57
58        Ok(ExampleHtmlArgs { themes })
59    }
60}
61
62#[proc_macro_attribute]
63pub fn example_html(args: TokenStream, token_stream: TokenStream) -> TokenStream {
64    let args = syn::parse::<ExampleHtmlArgs>(args).unwrap();
65
66    let themes = args
67        .themes
68        .or(Some(vec!["base16-ocean.dark".to_string()]))
69        .unwrap();
70
71    let fn_ = syn::parse::<syn::ItemFn>(token_stream.clone()).unwrap();
72
73    let sig = fn_.sig.clone();
74    let fn_name = quote! {#sig}.to_string();
75    let source_code = fn_
76        .block
77        .span()
78        .source_text()
79        .or(Some("".to_string()))
80        .expect("did not find function block source text");
81
82    let rendered_themes = render_themes(
83        fn_.sig.ident.to_string(),
84        format!("{fn_name} {source_code}"),
85        themes,
86    );
87    let original_tokens: proc_macro2::TokenStream = token_stream.into();
88
89    let out: TokenStream = quote! {
90        #rendered_themes
91        #original_tokens
92    }
93    .into();
94
95    out
96}
97
98fn render_themes(fn_name: String, code: String, themes: Vec<String>) -> proc_macro2::TokenStream {
99    let syntax_set = SyntaxSet::load_defaults_newlines();
100    let sr = syntax_set.find_syntax_by_extension("rs").unwrap();
101    let theme_set = ThemeSet::load_defaults();
102
103    let mut rendered_themes = vec![];
104
105    for theme_name in themes {
106        let mut theme = theme_set.themes[&theme_name].clone();
107
108        theme.settings.background = Some(Color {
109            r: 0,
110            g: 0,
111            b: 0,
112            a: 0,
113        });
114
115        let rendered_theme = highlighted_html_for_string(&code, &syntax_set, sr, &theme)
116            .unwrap()
117            .replace("style=\"background-color:#000000;\"", "");
118
119        rendered_themes.push(quote! {( #theme_name.to_string(), #rendered_theme.to_string())});
120    }
121
122    let fn_example_ident = Ident::new(
123        format!("{}_EXAMPLE_HTML_MAP", fn_name)
124            .to_uppercase()
125            .as_str(),
126        Span::call_site(),
127    );
128
129    quote! {
130        pub static #fn_example_ident: once_cell::sync::Lazy<std::collections::BTreeMap<String, String>> = once_cell::sync::Lazy::new(|| {
131             std::collections::BTreeMap::from([
132                #(#rendered_themes),*
133            ])
134        });
135    }
136}