temply_derive/
lib.rs

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/// Derive the `Template` trait.
15///
16/// The template can be specified with either `#[template]` or `#[template_inline]`.
17///
18/// Use the dedent option to automatically dedent content in blocks.
19///
20/// # Examples
21///
22/// ```ignore
23/// # use temply::Template;
24/// #[derive(Debug, Template)]
25/// #[template = "./hello.template"] // Path is relative to the src folder
26/// struct MyTemplate<'a> { name: &'a str }
27/// ```
28///
29/// ```ignore
30/// # use temply::Template;
31/// #[derive(Debug, Template)]
32/// #[template_inline = "Hello {{ name }}!"]
33/// struct MyTemplate<'a> { name: &'a str }
34/// ```
35#[proc_macro_derive(Template, attributes(template, template_inline, dedent))]
36pub fn derive_template(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
37    // Parse derive
38    let (name, data, generics, source, dedent) = parse_derive(input.into());
39
40    // Get source
41    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    // Lex and parse
50    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    // Dedent and trim
57    if dedent {
58        ws::dedent(&mut ast);
59    }
60    ws::trim(&mut ast);
61
62    // Generate
63    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}