1#![deny(rust_2018_idioms)]
2
3mod generator;
4mod lexer;
5mod parser;
6mod ws;
7
8use proc_macro2::TokenStream;
9use std::env;
10use std::fs;
11use std::path::{Path, PathBuf};
12use syn::{Data, DeriveInput, Generics, Ident, Lit, Meta};
13
14#[proc_macro_derive(Template, attributes(template, template_inline, dedent))]
36pub fn derive_template(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
37 let (name, data, generics, source, dedent) = parse_derive(input.into());
39
40 let (source, path) = match source {
42 Source::File(path) => (
43 fs::read_to_string(&path).expect("failed to read template from file"),
44 Some(path),
45 ),
46 Source::Inline(source) => (source, None),
47 };
48
49 let tokens = lexer::lex(&source);
51 let mut ast = match parser::parse(&source, &tokens) {
52 Ok(ast) => ast,
53 Err(error) => panic!("failed to parse template: {}", error.format(&source)),
54 };
55
56 if dedent {
58 ws::dedent(&mut ast);
59 }
60 ws::trim(&mut ast);
61
62 generator::generate(&name, &data, &generics, path.as_deref(), ast).into()
64}
65
66#[derive(Debug)]
67enum Source {
68 File(PathBuf),
69 Inline(String),
70}
71
72fn parse_derive(input: TokenStream) -> (Ident, Data, Generics, Source, bool) {
73 let ast = syn::parse2::<DeriveInput>(input).unwrap();
74
75 let root_path =
76 Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string())).join("src/");
77 let sources = ast
78 .attrs
79 .iter()
80 .filter_map(|attr| match attr.parse_meta() {
81 Ok(Meta::NameValue(name_value)) => {
82 if name_value.path.is_ident("template") {
83 match name_value.lit {
84 Lit::Str(str) => Some(Source::File(root_path.join(str.value()))),
85 _ => panic!("template must be a string"),
86 }
87 } else if name_value.path.is_ident("template_inline") {
88 match name_value.lit {
89 Lit::Str(str) => Some(Source::Inline(str.value())),
90 _ => panic!("template_inline must be a string"),
91 }
92 } else {
93 None
94 }
95 }
96 _ => None,
97 })
98 .collect::<Vec<_>>();
99 let source = if sources.len() == 1 {
100 sources.into_iter().next().unwrap()
101 } else {
102 panic!("found zero or more than one template source");
103 };
104 let dedent = ast.attrs.iter().any(|attr| match attr.parse_meta() {
105 Ok(Meta::Path(p)) => p.is_ident("dedent"),
106 _ => false,
107 });
108
109 (ast.ident, ast.data, ast.generics, source, dedent)
110}