#![allow(clippy::large_enum_variant)]
extern crate proc_macro;
use log::*;
use syn::Token;
mod derive_renderable;
mod inline_parse;
use crate::derive_renderable::*;
use anyhow::{anyhow, bail, Context, Error};
use html5ever::tendril::TendrilSink;
use kuchiki::NodeRef;
use proc_macro::TokenStream;
use quote::ToTokens;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Eq, PartialEq)]
enum TemplateSource {
Path(PathBuf),
Source(String),
}
#[derive(Debug, Clone)]
struct TemplateDerivation {
template_source: TemplateSource,
selector: String,
}
#[proc_macro_derive(WeftRenderable, attributes(template))]
pub fn derive_template(input: TokenStream) -> TokenStream {
env_logger::Builder::from_env("WEFT_LOG")
.try_init()
.unwrap_or_default();
let ast: syn::DeriveInput = syn::parse(input).unwrap();
match make_template(ast) {
Ok(toks) => toks.into(),
Err(err) => panic!("Error: {:?}", err),
}
}
fn make_template(item: syn::DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
info!("Deriving for {}", item.ident);
trace!("{:#?}", item);
let config = TemplateDerivation::from_derive(&item).context("find template")?;
let dom = config.load()?;
let impl_body = derive_impl(&config, dom, item)?;
Ok(impl_body.into_token_stream())
}
fn parse_path(path: &Path) -> Result<NodeRef, Error> {
info!("Using template from {:?}", path);
let parser = kuchiki::parse_html().from_utf8();
let root = parser
.from_file(path)
.with_context(|| format!("Parsing template from path {:?}", &path))?;
Ok(root)
}
fn parse_source(source: &str) -> kuchiki::NodeRef {
info!("Using inline template");
let parser = kuchiki::parse_html();
parser.one(source)
}
#[derive(Clone, Debug)]
enum TemplateArg {
Path(PathBuf),
Source(String),
Selector(String),
}
impl syn::parse::Parse for TemplateArg {
fn parse(buf: &syn::parse::ParseBuffer<'_>) -> Result<Self, syn::parse::Error> {
let id: syn::Ident = buf.parse()?;
let _eq: Token![=] = buf.parse()?;
if id == "path" {
let path: syn::LitStr = buf.parse()?;
Ok(TemplateArg::Path(PathBuf::from(path.value())))
} else if id == "source" {
let source: syn::LitStr = buf.parse()?;
Ok(TemplateArg::Source(source.value()))
} else if id == "selector" {
let selector: syn::LitStr = buf.parse()?;
Ok(TemplateArg::Selector(selector.value()))
} else {
Err(buf.error(format!("Unrecognised template parameter: {}", id)))
}
}
}
impl TemplateDerivation {
fn from_derive(item: &syn::DeriveInput) -> Result<TemplateDerivation, Error> {
let template_path = syn::parse_str::<syn::Path>("template")?;
let mut attrs = item
.attrs
.iter()
.filter(|a| a.path() == &template_path)
.inspect(|a| info!("Attribute: {:#?}", a));
let attr = attrs
.next()
.ok_or_else(|| anyhow!("Could not find template attribute"))?;
if attrs.next().is_some() {
bail!("Can only process a single #[template(…)] attribute")
}
let parser =
syn::punctuated::Punctuated::<TemplateArg, Token![,]>::parse_separated_nonempty;
let args = attr.parse_args_with(parser)?;
let mut path = None;
let mut source = None;
let mut template_selector = None;
for a in args {
match a {
TemplateArg::Path(p) => path = Some(p),
TemplateArg::Source(s) => source = Some(s),
TemplateArg::Selector(s) => template_selector = Some(s),
}
}
let template_source = match (path, source) {
(Some(path), None) => {
let path = root_dir().join(path);
TemplateSource::Path(path)
},
(None, Some(source)) => {
TemplateSource::Source(source)
},
_ => bail!("Exactly one of `source` or `path` attributes must be specfied in `#[template(...)]")
};
let selector = template_selector.unwrap_or_else(|| ":root".to_string());
let res = TemplateDerivation {
template_source,
selector,
};
Ok(res)
}
fn load(&self) -> Result<NodeRef, Error> {
let root = match &self.template_source {
TemplateSource::Path(ref path) => parse_path(path)?,
TemplateSource::Source(ref source) => parse_source(source),
};
let content = self
.find_root_from(root)
.ok_or_else(|| anyhow!("Could not locate root of parsed document?"))?;
Ok(content)
}
fn find_root_from(&self, node: NodeRef) -> Option<NodeRef> {
if let Ok(mut roots) = node.select(&self.selector) {
let first = roots.next()?;
if roots.next().is_some() {
warn!("Selector {} returns more than one match", self.selector);
return None;
}
return Some(first.as_node().clone());
}
None
}
}
fn root_dir() -> PathBuf {
std::env::var("CARGO_MANIFEST_DIR")
.unwrap_or_else(|_| {
warn!("Environment variable $CARGO_MANIFEST_DIR not set, assuming .");
".".into()
})
.into()
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
use syn::parse_quote;
#[test]
fn can_parse_with_path() {
let deriv = parse_quote!(
#[template(path = "../weft/tests/trivial.html")]
struct X;
);
let conf = TemplateDerivation::from_derive(&deriv).expect("parse derive");
let path = if let TemplateSource::Path(p) = conf.template_source {
std::fs::canonicalize(&p)
.with_context(|| format!("Path: {:?}", p))
.expect("Path must be canonicalisable")
} else {
panic!("Template source must be a Path(…); got: {:?}", conf)
};
let expected = std::fs::canonicalize("../weft/tests/trivial.html")
.expect("canonicalize expected path");
assert_eq!(path, expected);
}
#[test]
fn can_parse_with_source() {
let source = "<p>Stuff</p>";
let deriv = parse_quote!(
#[template(source = #source)]
struct X;
);
let conf = TemplateDerivation::from_derive(&deriv).expect("parse derive");
assert_eq!(conf.template_source, TemplateSource::Source(source.into()));
}
#[test]
fn cannot_parse_with_neither_source_or_path() {
let deriv = quote!(
#[template()]
struct X;
);
let parsed = syn::parse2(deriv.clone()).expect("parse");
let res = TemplateDerivation::from_derive(&parsed);
assert!(res.is_err(), "Template {} should not parse", deriv)
}
#[test]
fn cannot_parse_with_both_source_or_path() {
let deriv = quote!(
#[template(source = "...", path = "...")]
struct X;
);
let parsed = syn::parse2(deriv.clone()).expect("parse");
let res = TemplateDerivation::from_derive(&parsed);
assert!(res.is_err(), "Template {} should not parse", deriv)
}
#[test]
fn cannot_parse_with_multiple_attributes() {
let deriv = quote!(
#[template(source = "...")]
#[template(source = "...")]
struct X;
);
let parsed = syn::parse2(deriv.clone()).expect("parse");
let res = TemplateDerivation::from_derive(&parsed);
assert!(res.is_err(), "Template {} should not parse", deriv)
}
#[test]
#[should_panic]
fn cannot_parse_with_multiple_root_matches() {
env_logger::try_init().unwrap_or_default();
let _ = derive_template(
quote!(
#[template(source = "<p>foo</p><p>bar</p>", selector = "p")]
struct MultipleRoots;
)
.into(),
);
}
#[test]
fn will_extract_selector() {
let deriv = parse_quote!(
#[template(path = "hello.html", selector = "#hello-world")]
struct X;
);
let conf = TemplateDerivation::from_derive(&deriv).expect("parse derive");
assert_eq!(conf.selector, "#hello-world");
}
}