temply_derive/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#![deny(rust_2018_idioms)]

mod generator;
mod lexer;
mod parser;
mod ws;

use proc_macro2::TokenStream;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use syn::{Data, DeriveInput, Generics, Ident, Lit, Meta};

/// Derive the `Template` trait.
///
/// The template can be specified with either `#[template]` or `#[template_inline]`.
///
/// Use the dedent option to automatically dedent content in blocks.
///
/// # Examples
///
/// ```ignore
/// # use temply::Template;
/// #[derive(Debug, Template)]
/// #[template = "./hello.template"] // Path is relative to the src folder
/// struct MyTemplate<'a> { name: &'a str }
/// ```
///
/// ```ignore
/// # use temply::Template;
/// #[derive(Debug, Template)]
/// #[template_inline = "Hello {{ name }}!"]
/// struct MyTemplate<'a> { name: &'a str }
/// ```
#[proc_macro_derive(Template, attributes(template, template_inline, dedent))]
pub fn derive_template(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Parse derive
    let (name, data, generics, source, dedent) = parse_derive(input.into());

    // Get source
    let (source, path) = match source {
        Source::File(path) => (
            fs::read_to_string(&path).expect("failed to read template from file"),
            Some(path),
        ),
        Source::Inline(source) => (source, None),
    };

    // Lex and parse
    let tokens = lexer::lex(&source);
    let mut ast = match parser::parse(&source, &tokens) {
        Ok(ast) => ast,
        Err(error) => panic!("failed to parse template: {}", error.format(&source)),
    };

    // Dedent and trim
    if dedent {
        ws::dedent(&mut ast);
    }
    ws::trim(&mut ast);

    // Generate
    generator::generate(&name, &data, &generics, path.as_deref(), ast).into()
}

#[derive(Debug)]
enum Source {
    File(PathBuf),
    Inline(String),
}

fn parse_derive(input: TokenStream) -> (Ident, Data, Generics, Source, bool) {
    let ast = syn::parse2::<DeriveInput>(input).unwrap();

    let root_path =
        Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string())).join("src/");
    let sources = ast
        .attrs
        .iter()
        .filter_map(|attr| match attr.parse_meta() {
            Ok(Meta::NameValue(name_value)) => {
                if name_value.path.is_ident("template") {
                    match name_value.lit {
                        Lit::Str(str) => Some(Source::File(root_path.join(str.value()))),
                        _ => panic!("template must be a string"),
                    }
                } else if name_value.path.is_ident("template_inline") {
                    match name_value.lit {
                        Lit::Str(str) => Some(Source::Inline(str.value())),
                        _ => panic!("template_inline must be a string"),
                    }
                } else {
                    None
                }
            }
            _ => None,
        })
        .collect::<Vec<_>>();
    let source = if sources.len() == 1 {
        sources.into_iter().next().unwrap()
    } else {
        panic!("found zero or more than one template source");
    };
    let dedent = ast.attrs.iter().any(|attr| match attr.parse_meta() {
        Ok(Meta::Path(p)) => p.is_ident("dedent"),
        _ => false,
    });

    (ast.ident, ast.data, ast.generics, source, dedent)
}