//! Include inline HTML snippets //! //! See [`silkenweb_parse`] for details on the parsing. use std::{ env, fs, path::{Path, PathBuf}, }; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; use proc_macro_error::{abort_call_site, proc_macro_error}; use quote::quote; use silkenweb_parse::html_to_tokens; use syn::{parse_macro_input, LitStr}; /// Include an HTML snippet from a string literal. /// /// Take a string literal containing a single HTML element, and produce a /// [`Node`][silkenweb::node::Node] expression. The [`Dom`][silkenweb::dom::Dom] /// type is not specified, so if it can't be determined by type inference, you /// may need to provide a type annotation. /// /// See [`silkenweb_parse`] for details on the parsing. /// /// # Example /// /// ``` /// # use silkenweb_inline_html::inline_html; /// # use silkenweb::node::Node; /// let node: Node = inline_html!("

Inline HTML

"); /// assert_eq!(node.to_string(), "

Inline HTML

"); /// ``` #[proc_macro] #[proc_macro_error] pub fn inline_html(input: TokenStream) -> TokenStream { let html: LitStr = parse_macro_input!(input); let html_text = html.value(); let mut element_iter = html_to_tokens(quote! {D}.into(), &html_text).into_iter(); let element: proc_macro2::TokenStream = element_iter .next() .unwrap_or_else(|| abort_call_site!("Unable to parse any elements")) .into(); if element_iter.next().is_some() { abort_call_site!("Multiple elements found"); } quote! {{ pub fn node() -> ::silkenweb::node::Node { #element } node() }} .into() } /// Include an HTML snippet from a file. /// /// This takes a string literal as a filename, parses the contents of the file /// and puts the resulting expression into a funcion. The function name is /// derived from the filename by replacing non alphanumeric characters with an /// `_`. /// /// See `examples/inline-html` for a usage example. /// /// See [`silkenweb_parse`] for details on the parsing. #[proc_macro] #[proc_macro_error] pub fn html_file(input: TokenStream) -> TokenStream { let file: LitStr = parse_macro_input!(input); let file_path = root_dir().join(file.value()); html_from_path(&file_path).into() } /// Include HTML snippets from a directory of files. /// /// This takes a string literal as a directory name and is equivalent to running /// [`html_file!`] on every file directly contained in directory. /// /// See `examples/inline-html` for a usage example. /// /// See [`silkenweb_parse`] for details on the parsing. #[proc_macro] #[proc_macro_error] pub fn html_dir(input: TokenStream) -> TokenStream { let dir_literal: LitStr = parse_macro_input!(input); let dir = dir_literal.value(); let fns = fs::read_dir(root_dir().join(&dir)) .unwrap_or_else(|_| abort_call_site!("Unable to read dir '{}'", dir)) .filter_map(|entry| { let path = entry .unwrap_or_else(|_| abort_call_site!("Unable to read dir entry")) .path(); if path.is_file() { Some(html_from_path(&path)) } else { None } }); quote!(#(#fns)*).into() } fn html_from_path(file_path: &Path) -> proc_macro2::TokenStream { let html_text = fs::read_to_string(file_path) .unwrap_or_else(|_| abort_call_site!("Unable to read file '{:?}'", &file_path)); let mut element_iter = html_to_tokens(quote! {D}.into(), &html_text).into_iter(); let element: proc_macro2::TokenStream = element_iter .next() .unwrap_or_else(|| abort_call_site!("Unable to parse any elements for '{:?}'", &file_path)) .into(); if element_iter.next().is_some() { abort_call_site!("Multiple elements found in '{:?}'", &file_path); } let fn_name = filename_to_ident( file_path .file_stem() .unwrap_or_else(|| { abort_call_site!("Unable to extract file stem from '{:?}'", file_path) }) .to_str() .unwrap(), ); quote! { pub fn #fn_name() -> ::silkenweb::node::Node { #element } } } fn root_dir() -> PathBuf { const CARGO_MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR"; PathBuf::from( env::var(CARGO_MANIFEST_DIR) .unwrap_or_else(|_| abort_call_site!("Couldn't read '{CARGO_MANIFEST_DIR}' variable")), ) } fn filename_to_ident(file: &str) -> Ident { let ident = file.replace(|c: char| !c.is_alphanumeric(), "_"); if let Some(first) = ident.chars().next() { if !first.is_alphabetic() && first != '_' { abort_call_site!("Illegal first char in '{}'", ident); } } else { abort_call_site!("Empty identifier"); } Ident::new(&ident, Span::call_site()) }