silkenweb_inline_html/
lib.rs

1//! Include inline HTML snippets
2//!
3//! See [`silkenweb_parse`] for details on the parsing.
4use std::{
5    env, fs,
6    path::{Path, PathBuf},
7};
8
9use proc_macro::TokenStream;
10use proc_macro2::{Ident, Span};
11use proc_macro_error::{abort_call_site, proc_macro_error};
12use quote::quote;
13use silkenweb_parse::html_to_tokens;
14use syn::{parse_macro_input, LitStr};
15
16/// Include an HTML snippet from a string literal.
17///
18/// Take a string literal containing a single HTML element, and produce a
19/// [`Node`][silkenweb::node::Node] expression. The [`Dom`][silkenweb::dom::Dom]
20/// type is not specified, so if it can't be determined by type inference, you
21/// may need to provide a type annotation.
22///
23/// See [`silkenweb_parse`] for details on the parsing.
24///
25/// # Example
26///
27/// ```
28/// # use silkenweb_inline_html::inline_html;
29/// # use silkenweb::node::Node;
30/// let node: Node = inline_html!("<p>Inline HTML</p>");
31/// assert_eq!(node.to_string(), "<p>Inline HTML</p>");
32/// ```
33#[proc_macro]
34#[proc_macro_error]
35pub fn inline_html(input: TokenStream) -> TokenStream {
36    let html: LitStr = parse_macro_input!(input);
37    let html_text = html.value();
38    let mut element_iter = html_to_tokens(quote! {D}.into(), &html_text).into_iter();
39    let element: proc_macro2::TokenStream = element_iter
40        .next()
41        .unwrap_or_else(|| abort_call_site!("Unable to parse any elements"))
42        .into();
43
44    if element_iter.next().is_some() {
45        abort_call_site!("Multiple elements found");
46    }
47
48    quote! {{
49        pub fn node<D: ::silkenweb::dom::Dom>() -> ::silkenweb::node::Node<D> {
50            #element
51        }
52
53        node()
54    }}
55    .into()
56}
57
58/// Include an HTML snippet from a file.
59///
60/// This takes a string literal as a filename, parses the contents of the file
61/// and puts the resulting expression into a funcion. The function name is
62/// derived from the filename by replacing non alphanumeric characters with an
63/// `_`.
64///
65/// See `examples/inline-html` for a usage example.
66///
67/// See [`silkenweb_parse`] for details on the parsing.
68#[proc_macro]
69#[proc_macro_error]
70pub fn html_file(input: TokenStream) -> TokenStream {
71    let file: LitStr = parse_macro_input!(input);
72    let file_path = root_dir().join(file.value());
73    html_from_path(&file_path).into()
74}
75
76/// Include HTML snippets from a directory of files.
77///
78/// This takes a string literal as a directory name and is equivalent to running
79/// [`html_file!`] on every file directly contained in directory.
80///
81/// See `examples/inline-html` for a usage example.
82///
83/// See [`silkenweb_parse`] for details on the parsing.
84#[proc_macro]
85#[proc_macro_error]
86pub fn html_dir(input: TokenStream) -> TokenStream {
87    let dir_literal: LitStr = parse_macro_input!(input);
88    let dir = dir_literal.value();
89    let fns = fs::read_dir(root_dir().join(&dir))
90        .unwrap_or_else(|_| abort_call_site!("Unable to read dir '{}'", dir))
91        .filter_map(|entry| {
92            let path = entry
93                .unwrap_or_else(|_| abort_call_site!("Unable to read dir entry"))
94                .path();
95
96            if path.is_file() {
97                Some(html_from_path(&path))
98            } else {
99                None
100            }
101        });
102
103    quote!(#(#fns)*).into()
104}
105
106fn html_from_path(file_path: &Path) -> proc_macro2::TokenStream {
107    let html_text = fs::read_to_string(file_path)
108        .unwrap_or_else(|_| abort_call_site!("Unable to read file '{:?}'", &file_path));
109    let mut element_iter = html_to_tokens(quote! {D}.into(), &html_text).into_iter();
110    let element: proc_macro2::TokenStream = element_iter
111        .next()
112        .unwrap_or_else(|| abort_call_site!("Unable to parse any elements for '{:?}'", &file_path))
113        .into();
114
115    if element_iter.next().is_some() {
116        abort_call_site!("Multiple elements found in '{:?}'", &file_path);
117    }
118
119    let fn_name = filename_to_ident(
120        file_path
121            .file_stem()
122            .unwrap_or_else(|| {
123                abort_call_site!("Unable to extract file stem from '{:?}'", file_path)
124            })
125            .to_str()
126            .unwrap(),
127    );
128
129    quote! {
130        pub fn #fn_name<D: ::silkenweb::dom::Dom>() -> ::silkenweb::node::Node<D> {
131            #element
132        }
133    }
134}
135
136fn root_dir() -> PathBuf {
137    const CARGO_MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR";
138
139    PathBuf::from(
140        env::var(CARGO_MANIFEST_DIR)
141            .unwrap_or_else(|_| abort_call_site!("Couldn't read '{CARGO_MANIFEST_DIR}' variable")),
142    )
143}
144
145fn filename_to_ident(file: &str) -> Ident {
146    let ident = file.replace(|c: char| !c.is_alphanumeric(), "_");
147
148    if let Some(first) = ident.chars().next() {
149        if !first.is_alphabetic() && first != '_' {
150            abort_call_site!("Illegal first char in '{}'", ident);
151        }
152    } else {
153        abort_call_site!("Empty identifier");
154    }
155
156    Ident::new(&ident, Span::call_site())
157}