askama_derive/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
2#![deny(elided_lifetimes_in_paths)]
3#![deny(unreachable_pub)]
4
5mod config;
6mod generator;
7mod heritage;
8mod html;
9mod input;
10mod integration;
11#[cfg(test)]
12mod tests;
13
14use std::borrow::{Borrow, Cow};
15use std::collections::hash_map::{Entry, HashMap};
16use std::fmt;
17use std::hash::{BuildHasher, Hash};
18use std::path::Path;
19use std::sync::Mutex;
20
21use parser::{Parsed, ascii_str, strip_common};
22#[cfg(not(feature = "__standalone"))]
23use proc_macro::TokenStream as TokenStream12;
24#[cfg(feature = "__standalone")]
25use proc_macro2::TokenStream as TokenStream12;
26use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree};
27use quote::{quote, quote_spanned};
28use rustc_hash::FxBuildHasher;
29
30use crate::config::{Config, read_config_file};
31use crate::generator::{TmplKind, template_to_string};
32use crate::heritage::{Context, Heritage};
33use crate::input::{AnyTemplateArgs, Print, TemplateArgs, TemplateInput};
34use crate::integration::{Buffer, build_template_enum};
35
36/// The `Template` derive macro and its `template()` attribute.
37///
38/// Askama works by generating one or more trait implementations for any
39/// `struct` type decorated with the `#[derive(Template)]` attribute. The
40/// code generation process takes some options that can be specified through
41/// the `template()` attribute.
42///
43/// ## Attributes
44///
45/// The following sub-attributes are currently recognized:
46///
47/// ### path
48///
49/// E.g. `path = "foo.html"`
50///
51/// Sets the path to the template file.
52/// The path is interpreted as relative to the configured template directories
53/// (by default, this is a `templates` directory next to your `Cargo.toml`).
54/// The file name extension is used to infer an escape mode (see below). In
55/// web framework integrations, the path's extension may also be used to
56/// infer the content type of the resulting response.
57/// Cannot be used together with `source`.
58///
59/// ### source
60///
61/// E.g. `source = "{{ foo }}"`
62///
63/// Directly sets the template source.
64/// This can be useful for test cases or short templates. The generated path
65/// is undefined, which generally makes it impossible to refer to this
66/// template from other templates. If `source` is specified, `ext` must also
67/// be specified (see below). Cannot be used together with `path`.
68/// `ext` (e.g. `ext = "txt"`): lets you specify the content type as a file
69/// extension. This is used to infer an escape mode (see below), and some
70/// web framework integrations use it to determine the content type.
71/// Cannot be used together with `path`.
72///
73/// ### `in_doc`
74///
75/// E.g. `in_doc = true`
76///
77/// As an alternative to supplying the code template code in an external file (as `path` argument),
78/// or as a string (as `source` argument), you can also enable the `"code-in-doc"` feature.
79/// With this feature, you can specify the template code directly in the documentation
80/// of the template `struct`.
81///
82/// Instead of `path = "…"` or `source = "…"`, specify `in_doc = true` in the `#[template]`
83/// attribute, and in the struct's documentation add a `askama` code block:
84///
85/// ```rust,ignore
86/// /// ```askama
87/// /// <div>{{ lines|linebreaksbr }}</div>
88/// /// ```
89/// #[derive(Template)]
90/// #[template(ext = "html", in_doc = true)]
91/// struct Example<'a> {
92///     lines: &'a str,
93/// }
94/// ```
95///
96/// ### print
97///
98/// E.g. `print = "code"`
99///
100/// Enable debugging by printing nothing (`none`), the parsed syntax tree (`ast`),
101/// the generated code (`code`) or `all` for both.
102/// The requested data will be printed to stdout at compile time.
103///
104/// ### block
105///
106/// E.g. `block = "block_name"`
107///
108/// Renders the block by itself.
109/// Expressions outside of the block are not required by the struct, and
110/// inheritance is also supported. This can be useful when you need to
111/// decompose your template for partial rendering, without needing to
112/// extract the partial into a separate template or macro.
113///
114/// ```rust,ignore
115/// #[derive(Template)]
116/// #[template(path = "hello.html", block = "hello")]
117/// struct HelloTemplate<'a> { ... }
118/// ```
119///
120/// ### blocks
121///
122/// E.g. `blocks = ["title", "content"]`
123///
124/// Automatically generates (a number of) sub-templates that act as if they had a
125/// `block = "..."` attribute. You can access the sub-templates with the method
126/// <code>my_template.as_<em>block_name</em>()</code>, where *`block_name`* is the
127/// name of the block:
128///
129/// ```rust,ignore
130/// #[derive(Template)]
131/// #[template(
132///     ext = "txt",
133///     source = "
134///         {% block title %} ... {% endblock %}
135///         {% block content %} ... {% endblock %}
136///     ",
137///     blocks = ["title", "content"]
138/// )]
139/// struct News<'a> {
140///     title: &'a str,
141///     message: &'a str,
142/// }
143///
144/// let news = News {
145///     title: "Announcing Rust 1.84.1",
146///     message: "The Rust team has published a new point release of Rust, 1.84.1.",
147/// };
148/// assert_eq!(
149///     news.as_title().render().unwrap(),
150///     "<h1>Announcing Rust 1.84.1</h1>"
151/// );
152/// ```
153///
154/// ### escape
155///
156/// E.g. `escape = "none"`
157///
158/// Override the template's extension used for the purpose of determining the escaper for
159/// this template. See the section on configuring custom escapers for more information.
160///
161/// ### syntax
162///
163/// E.g. `syntax = "foo"`
164///
165/// Set the syntax name for a parser defined in the configuration file.
166/// The default syntax, `"default"`,  is the one provided by Askama.
167///
168/// ### askama
169///
170/// E.g. `askama = askama`
171///
172/// If you are using askama in a subproject, a library or a [macro][book-macro], it might be
173/// necessary to specify the [path][book-tree] where to find the module `askama`:
174///
175/// [book-macro]: https://doc.rust-lang.org/book/ch19-06-macros.html
176/// [book-tree]: https://doc.rust-lang.org/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html
177///
178/// ```rust,ignore
179/// #[doc(hidden)]
180/// use askama as __askama;
181///
182/// #[macro_export]
183/// macro_rules! new_greeter {
184///     ($name:ident) => {
185///         #[derive(Debug, $crate::askama::Template)]
186///         #[template(
187///             ext = "txt",
188///             source = "Hello, world!",
189///             askama = $crate::__askama
190///         )]
191///         struct $name;
192///     }
193/// }
194///
195/// new_greeter!(HelloWorld);
196/// assert_eq!(HelloWorld.to_string(), Ok("Hello, world."));
197/// ```
198#[allow(clippy::useless_conversion)] // To be compatible with both `TokenStream`s
199#[cfg_attr(
200    not(feature = "__standalone"),
201    proc_macro_derive(Template, attributes(template))
202)]
203#[must_use]
204pub fn derive_template(input: TokenStream12) -> TokenStream12 {
205    let ast = match syn::parse2(input.into()) {
206        Ok(ast) => ast,
207        Err(err) => {
208            let msgs = err.into_iter().map(|err| err.to_string());
209            let ts = quote! {
210                const _: () = {
211                    extern crate core;
212                    #(core::compile_error!(#msgs);)*
213                };
214            };
215            return ts.into();
216        }
217    };
218
219    let mut buf = Buffer::new();
220    let mut args = AnyTemplateArgs::new(&ast);
221    let crate_name = args
222        .as_mut()
223        .map(|a| a.take_crate_name())
224        .unwrap_or_default();
225
226    let result = args.and_then(|args| build_template(&mut buf, &ast, args));
227    let ts = if let Err(CompileError { msg, span }) = result {
228        let mut ts = quote_spanned! {
229            span.unwrap_or(ast.ident.span()) =>
230            askama::helpers::core::compile_error!(#msg);
231        };
232        buf.clear();
233        if build_skeleton(&mut buf, &ast).is_ok() {
234            let source: TokenStream = buf.into_string().parse().unwrap();
235            ts.extend(source);
236        }
237        ts
238    } else {
239        buf.into_string().parse().unwrap()
240    };
241
242    let ts = TokenTree::Group(Group::new(Delimiter::None, ts));
243    let ts = if let Some(crate_name) = crate_name {
244        quote! {
245            const _: () = {
246                use #crate_name as askama;
247                #ts
248            };
249        }
250    } else {
251        quote! {
252            const _: () = {
253                extern crate askama;
254                #ts
255            };
256        }
257    };
258    ts.into()
259}
260
261fn build_skeleton(buf: &mut Buffer, ast: &syn::DeriveInput) -> Result<usize, CompileError> {
262    let template_args = TemplateArgs::fallback();
263    let config = Config::new("", None, None, None, None)?;
264    let input = TemplateInput::new(ast, None, config, &template_args)?;
265    let mut contexts = HashMap::default();
266    let parsed = parser::Parsed::default();
267    contexts.insert(&input.path, Context::empty(&parsed));
268    template_to_string(buf, &input, &contexts, None, TmplKind::Struct)
269}
270
271/// Takes a `syn::DeriveInput` and generates source code for it
272///
273/// Reads the metadata from the `template()` attribute to get the template
274/// metadata, then fetches the source from the filesystem. The source is
275/// parsed, and the parse tree is fed to the code generator. Will print
276/// the parse tree and/or generated source according to the `print` key's
277/// value as passed to the `template()` attribute.
278pub(crate) fn build_template(
279    buf: &mut Buffer,
280    ast: &syn::DeriveInput,
281    args: AnyTemplateArgs,
282) -> Result<usize, CompileError> {
283    let err_span;
284    let mut result = match args {
285        AnyTemplateArgs::Struct(item) => {
286            err_span = item.source.1.or(item.template_span);
287            build_template_item(buf, ast, None, &item, TmplKind::Struct)
288        }
289        AnyTemplateArgs::Enum {
290            enum_args,
291            vars_args,
292            has_default_impl,
293        } => {
294            err_span = enum_args
295                .as_ref()
296                .and_then(|v| v.source.as_ref())
297                .map(|s| s.span())
298                .or_else(|| enum_args.as_ref().map(|v| v.template.span()));
299            build_template_enum(buf, ast, enum_args, vars_args, has_default_impl)
300        }
301    };
302    if let Err(err) = &mut result {
303        if err.span.is_none() {
304            err.span = err_span;
305        }
306    }
307    result
308}
309
310fn build_template_item(
311    buf: &mut Buffer,
312    ast: &syn::DeriveInput,
313    enum_ast: Option<&syn::DeriveInput>,
314    template_args: &TemplateArgs,
315    tmpl_kind: TmplKind<'_>,
316) -> Result<usize, CompileError> {
317    let config_path = template_args.config_path();
318    let (s, full_config_path) = read_config_file(config_path, template_args.config_span)?;
319    let config = Config::new(
320        &s,
321        config_path,
322        template_args.whitespace,
323        template_args.config_span,
324        full_config_path,
325    )?;
326    let input = TemplateInput::new(ast, enum_ast, config, template_args)?;
327
328    let mut templates = HashMap::default();
329    input.find_used_templates(&mut templates)?;
330
331    let mut contexts = HashMap::default();
332    for (path, parsed) in &templates {
333        contexts.insert(path, Context::new(input.config, path, parsed)?);
334    }
335
336    let ctx = &contexts[&input.path];
337    let heritage = if !ctx.blocks.is_empty() || ctx.extends.is_some() {
338        Some(Heritage::new(ctx, &contexts))
339    } else {
340        None
341    };
342
343    if let Some((block_name, block_span)) = input.block {
344        let has_block = match &heritage {
345            Some(heritage) => heritage.blocks.contains_key(block_name),
346            None => ctx.blocks.contains_key(block_name),
347        };
348        if !has_block {
349            return Err(CompileError::no_file_info(
350                format_args!("cannot find block `{block_name}`"),
351                Some(block_span),
352            ));
353        }
354    }
355
356    if input.print == Print::Ast || input.print == Print::All {
357        eprintln!("{:?}", templates[&input.path].nodes());
358    }
359
360    let mark = buf.get_mark();
361    let size_hint = template_to_string(buf, &input, &contexts, heritage.as_ref(), tmpl_kind)?;
362    if input.print == Print::Code || input.print == Print::All {
363        eprintln!("{}", buf.marked_text(mark));
364    }
365    Ok(size_hint)
366}
367
368#[derive(Debug, Clone)]
369struct CompileError {
370    msg: String,
371    span: Option<Span>,
372}
373
374impl CompileError {
375    fn new<S: fmt::Display>(msg: S, file_info: Option<FileInfo<'_>>) -> Self {
376        Self::new_with_span(msg, file_info, None)
377    }
378
379    fn new_with_span<S: fmt::Display>(
380        msg: S,
381        file_info: Option<FileInfo<'_>>,
382        span: Option<Span>,
383    ) -> Self {
384        let msg = match file_info {
385            Some(file_info) => format!("{msg}{file_info}"),
386            None => msg.to_string(),
387        };
388        Self { msg, span }
389    }
390
391    fn no_file_info<S: ToString>(msg: S, span: Option<Span>) -> Self {
392        Self {
393            msg: msg.to_string(),
394            span,
395        }
396    }
397}
398
399impl std::error::Error for CompileError {}
400
401impl fmt::Display for CompileError {
402    #[inline]
403    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
404        fmt.write_str(&self.msg)
405    }
406}
407
408#[derive(Debug, Clone, Copy)]
409struct FileInfo<'a> {
410    path: &'a Path,
411    source: Option<&'a str>,
412    node_source: Option<&'a str>,
413}
414
415impl<'a> FileInfo<'a> {
416    fn new(path: &'a Path, source: Option<&'a str>, node_source: Option<&'a str>) -> Self {
417        Self {
418            path,
419            source,
420            node_source,
421        }
422    }
423
424    fn of(node: parser::Span<'a>, path: &'a Path, parsed: &'a Parsed) -> Self {
425        let source = parsed.source();
426        Self {
427            path,
428            source: Some(source),
429            node_source: node.as_suffix_of(source),
430        }
431    }
432}
433
434impl fmt::Display for FileInfo<'_> {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        if let (Some(source), Some(node_source)) = (self.source, self.node_source) {
437            let (error_info, file_path) = generate_error_info(source, node_source, self.path);
438            write!(
439                f,
440                "\n  --> {file_path}:{row}:{column}\n{source_after}",
441                row = error_info.row,
442                column = error_info.column,
443                source_after = error_info.source_after,
444            )
445        } else {
446            write!(
447                f,
448                "\n --> {}",
449                match std::env::current_dir() {
450                    Ok(cwd) => fmt_left!(move "{}", strip_common(&cwd, self.path)),
451                    Err(_) => fmt_right!("{}", self.path.display()),
452                }
453            )
454        }
455    }
456}
457
458struct ErrorInfo {
459    row: usize,
460    column: usize,
461    source_after: String,
462}
463
464fn generate_row_and_column(src: &str, input: &str) -> ErrorInfo {
465    const MAX_LINE_LEN: usize = 80;
466
467    let offset = src.len() - input.len();
468    let (source_before, source_after) = src.split_at(offset);
469
470    let source_after = match source_after
471        .char_indices()
472        .enumerate()
473        .take(MAX_LINE_LEN + 1)
474        .last()
475    {
476        Some((MAX_LINE_LEN, (i, _))) => format!("{:?}...", &source_after[..i]),
477        _ => format!("{source_after:?}"),
478    };
479
480    let (row, last_line) = source_before.lines().enumerate().last().unwrap_or_default();
481    let column = last_line.chars().count();
482    ErrorInfo {
483        row: row + 1,
484        column,
485        source_after,
486    }
487}
488
489/// Return the error related information and its display file path.
490fn generate_error_info(src: &str, input: &str, file_path: &Path) -> (ErrorInfo, String) {
491    let file_path = match std::env::current_dir() {
492        Ok(cwd) => strip_common(&cwd, file_path),
493        Err(_) => file_path.display().to_string(),
494    };
495    let error_info = generate_row_and_column(src, input);
496    (error_info, file_path)
497}
498
499struct MsgValidEscapers<'a>(&'a [(Vec<Cow<'a, str>>, Cow<'a, str>)]);
500
501impl fmt::Display for MsgValidEscapers<'_> {
502    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503        let mut exts = self
504            .0
505            .iter()
506            .flat_map(|(exts, _)| exts)
507            .map(|x| format!("{x:?}"))
508            .collect::<Vec<_>>();
509        exts.sort();
510        write!(f, "The available extensions are: {}", exts.join(", "))
511    }
512}
513
514#[derive(Debug)]
515struct OnceMap<K, V>([Mutex<HashMap<K, V, FxBuildHasher>>; 8]);
516
517impl<K, V> Default for OnceMap<K, V> {
518    fn default() -> Self {
519        Self(Default::default())
520    }
521}
522
523impl<K: Hash + Eq, V> OnceMap<K, V> {
524    // The API of this function was copied, and adapted from the `once_map` crate
525    // <https://crates.io/crates/once_map/0.4.18>.
526    fn get_or_try_insert<T, Q, E>(
527        &self,
528        key: &Q,
529        make_key_value: impl FnOnce(&Q) -> Result<(K, V), E>,
530        to_value: impl FnOnce(&V) -> T,
531    ) -> Result<T, E>
532    where
533        K: Borrow<Q>,
534        Q: Hash + Eq,
535    {
536        let shard_idx = (FxBuildHasher.hash_one(key) % self.0.len() as u64) as usize;
537        let mut shard = self.0[shard_idx].lock().unwrap();
538        Ok(to_value(if let Some(v) = shard.get(key) {
539            v
540        } else {
541            let (k, v) = make_key_value(key)?;
542            match shard.entry(k) {
543                Entry::Vacant(entry) => entry.insert(v),
544                Entry::Occupied(_) => unreachable!("key in map when it should not have been"),
545            }
546        }))
547    }
548}
549
550enum EitherFormat<L, R>
551where
552    L: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
553    R: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
554{
555    Left(L),
556    Right(R),
557}
558
559impl<L, R> fmt::Display for EitherFormat<L, R>
560where
561    L: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
562    R: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
563{
564    #[inline]
565    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566        match self {
567            Self::Left(v) => v(f),
568            Self::Right(v) => v(f),
569        }
570    }
571}
572
573macro_rules! fmt_left {
574    (move $fmt:literal $($tt:tt)*) => {
575        $crate::EitherFormat::Left(move |f: &mut std::fmt::Formatter<'_>| {
576            write!(f, $fmt $($tt)*)
577        })
578    };
579    ($fmt:literal $($tt:tt)*) => {
580        $crate::EitherFormat::Left(|f: &mut std::fmt::Formatter<'_>| {
581            write!(f, $fmt $($tt)*)
582        })
583    };
584}
585
586macro_rules! fmt_right {
587    (move $fmt:literal $($tt:tt)*) => {
588        $crate::EitherFormat::Right(move |f: &mut std::fmt::Formatter<'_>| {
589            write!(f, $fmt $($tt)*)
590        })
591    };
592    ($fmt:literal $($tt:tt)*) => {
593        $crate::EitherFormat::Right(|f: &mut std::fmt::Formatter<'_>| {
594            write!(f, $fmt $($tt)*)
595        })
596    };
597}
598
599pub(crate) use {fmt_left, fmt_right};
600
601// This is used by the code generator to decide whether a named filter is part of
602// Askama or should refer to a local `filters` module.
603const BUILTIN_FILTERS: &[&str] = &[
604    "capitalize",
605    "center",
606    "indent",
607    "lower",
608    "lowercase",
609    "title",
610    "trim",
611    "truncate",
612    "upper",
613    "uppercase",
614    "wordcount",
615];
616
617// Built-in filters that need the `alloc` feature.
618const BUILTIN_FILTERS_NEED_ALLOC: &[&str] = &["center", "truncate"];