example_html_highlight_macro/
lib.rs1use 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}