#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(elided_lifetimes_in_paths)]
#![deny(unreachable_pub)]
#![cfg_attr(
all(feature = "external-sources", feature = "nightly-spans"),
feature(proc_macro_def_site, proc_macro_expand)
)]
extern crate proc_macro;
mod ascii_str;
mod config;
mod filter_fn;
mod generator;
mod heritage;
mod html;
mod input;
mod integration;
mod spans;
#[cfg(test)]
mod tests;
#[doc(hidden)]
#[cfg(feature = "proc-macro")]
pub mod __macro_support {
pub use proc_macro::TokenStream as TokenStream1;
pub use proc_macro2::TokenStream as TokenStream2;
pub use quote::quote;
}
use std::borrow::{Borrow, Cow};
use std::collections::hash_map::Entry;
use std::fmt;
use std::hash::{BuildHasher, Hash};
use std::ops::ControlFlow;
use std::path::Path;
use std::sync::Mutex;
use parser::{Parsed, is_rust_keyword, strip_common};
use proc_macro2::{Literal, Span, TokenStream};
use quote::{ToTokens, quote, quote_spanned};
use rustc_hash::FxBuildHasher;
use syn::parse::Parse;
use syn::spanned::Spanned;
use syn::{Ident, parse2};
use crate::config::{Config, read_config_file};
pub use crate::filter_fn::derive_filter_fn;
use crate::generator::{TmplKind, template_to_string};
use crate::heritage::{Context, Heritage};
use crate::input::{AnyTemplateArgs, Print, TemplateArgs, TemplateInput};
use crate::integration::{Buffer, build_template_enum};
pub const CAN_USE_EXTERNAL_SOURCES: bool = cfg!(feature = "external-sources");
#[macro_export]
#[cfg(feature = "proc-macro")]
macro_rules! make_derive_template {
(
$(#[$meta:meta])*
$vis:vis fn $name:ident() {
$($import:stmt)+
}
) => {
$(#[$meta])*
$vis fn $name(
input: $crate::__macro_support::TokenStream1,
) -> $crate::__macro_support::TokenStream1 {
fn import_askama() -> $crate::__macro_support::TokenStream2 {
$crate::__macro_support::quote!($($import)*)
}
$crate::derive_template(input.into(), import_askama).into()
}
};
}
#[macro_export]
#[cfg(feature = "proc-macro")]
macro_rules! make_filter_fn {
(
$(#[$meta:meta])*
$vis:vis fn $name:ident() {
$($import:stmt)+
}
) => {
$(#[$meta])*
$vis fn $name(
attr: $crate::__macro_support::TokenStream1,
item: $crate::__macro_support::TokenStream1,
) -> $crate::__macro_support::TokenStream1 {
fn import_askama() -> $crate::__macro_support::TokenStream2 {
$crate::__macro_support::quote!($($import)*)
}
$crate::derive_filter_fn(attr.into(), item.into(), import_askama).into()
}
}
}
pub fn derive_template(input: TokenStream, import_askama: fn() -> TokenStream) -> TokenStream {
let ast = match parse_ts_or_compile_error(input, import_askama) {
ControlFlow::Continue(ast) => ast,
ControlFlow::Break(err) => return err,
};
let mut buf = Buffer::new();
let mut args = AnyTemplateArgs::new(&ast);
let crate_name = args
.as_mut()
.map(|a| a.take_crate_name())
.unwrap_or_default();
let ts = match args.and_then(|args| build_template(&mut buf, &ast, args)) {
Ok(_) => buf.into_token_stream(),
Err(CompileError { msg, span }) => {
let mut ts = quote::quote_spanned! {
span.unwrap_or(ast.ident.span()) =>
askama::helpers::core::compile_error!(#msg);
};
buf.clear();
if build_skeleton(&mut buf, &ast).is_ok() {
let source: TokenStream = buf.into_token_stream();
ts.extend(source);
}
ts
}
};
let import_askama = match crate_name {
Some(crate_name) => quote!(use #crate_name as askama;),
None => import_askama(),
};
quote_spanned! {
ast.ident.span() =>
#[allow(
// We use `Struct { 0: arg0, 1: arg1 }` in enum specialization.
clippy::init_numbered_fields, non_shorthand_field_patterns,
// The generated code is not indented at all.
clippy::suspicious_else_formatting,
// We don't care if the user does not use `Template`, `FastWritable`, etc.
dead_code,
// We intentionally add extraneous underscores in type and variable names.
non_camel_case_types, non_snake_case,
// We have too little context information to generate better code.
// The generated source does not have to be perfect, anyway.
clippy::double_parens, clippy::identity_op, clippy::into_iter_on_ref,
clippy::needless_borrow, clippy::needless_borrows_for_generic_args,
clippy::nonminimal_bool, clippy::op_ref, clippy::useless_conversion, unused_braces,
unused_parens,
)]
const _: () = {
#import_askama
#ts
};
}
}
fn parse_ts_or_compile_error<T: Parse>(
input: TokenStream,
import_askama: fn() -> TokenStream,
) -> ControlFlow<TokenStream, T> {
match parse2(input) {
Ok(ast) => ControlFlow::Continue(ast),
Err(err) => {
let import_askama = import_askama();
let msgs = err.into_iter().map(|err| err.to_string());
ControlFlow::Break(quote! {
const _: () = {
#import_askama
#(core::compile_error!(#msgs);)*
};
})
}
}
}
fn build_skeleton(buf: &mut Buffer, ast: &syn::DeriveInput) -> Result<SizeHint, CompileError> {
let template_args = TemplateArgs::fallback();
let config = Config::new("", None, None, None, None)?;
let input = TemplateInput::new(ast, None, config, &template_args)?;
let mut contexts = HashMap::default();
let parsed = parser::Parsed::default();
contexts.insert(&input.path, Context::empty(&parsed, ast.span()));
template_to_string(buf, &input, &contexts, None, TmplKind::Struct)
}
pub(crate) fn build_template(
buf: &mut Buffer,
ast: &syn::DeriveInput,
args: AnyTemplateArgs,
) -> Result<SizeHint, CompileError> {
let err_span;
let mut result = match args {
AnyTemplateArgs::Struct(item) => {
err_span = Some(item.source.1.config_span());
build_template_item(buf, ast, None, &item, TmplKind::Struct)
}
AnyTemplateArgs::Enum {
enum_args,
vars_args,
has_default_impl,
} => {
err_span = enum_args
.as_ref()
.and_then(|v| v.source.as_ref())
.map(|s| s.span())
.or_else(|| enum_args.as_ref().map(|v| v.template_span));
build_template_enum(buf, ast, enum_args, vars_args, has_default_impl)
}
};
if let Err(err) = &mut result
&& err.span.is_none()
{
err.span = err_span;
}
result
}
#[derive(Default)]
pub(crate) struct CalledBlocks<'a> {
pub(crate) called_blocks: HashMap<&'a str, Vec<FileInfo<'a>>>,
pub(crate) unprocessed: Vec<(&'a str, FileInfo<'a>)>,
}
impl CalledBlocks<'_> {
fn check_if_already_called(&self, block_name: &str, current: FileInfo<'_>) {
if let Some(calls) = self.called_blocks.get(&block_name)
&& let Some(prev) = calls.iter().skip(1).last()
{
crate::heritage::duplicated_block_call(current, block_name, prev);
}
}
}
fn build_template_item(
buf: &mut Buffer,
ast: &syn::DeriveInput,
enum_ast: Option<&syn::DeriveInput>,
template_args: &TemplateArgs,
tmpl_kind: TmplKind<'_>,
) -> Result<SizeHint, CompileError> {
let config_path = template_args.config_path();
let (s, full_config_path) = read_config_file(config_path, template_args.config_span)?;
let config = Config::new(
&s,
config_path,
template_args.whitespace,
template_args.config_span,
full_config_path,
)?;
let input = TemplateInput::new(ast, enum_ast, config, template_args)?;
let mut templates = HashMap::default();
input.find_used_templates(&mut templates)?;
let mut contexts = HashMap::default();
let mut called_blocks = CalledBlocks::default();
for (path, parsed) in &templates {
contexts.insert(
path,
Context::new(
input.config,
path,
parsed,
input.source_span.clone(),
input.template_span,
&mut called_blocks,
)?,
);
}
let mut unprocessed_items = std::mem::take(&mut called_blocks.unprocessed);
while let Some((name, file_info)) = unprocessed_items.pop() {
called_blocks
.called_blocks
.entry(name)
.or_default()
.push(file_info);
}
let ctx = &contexts[&input.path];
let heritage = if !ctx.blocks.is_empty() || ctx.extends.is_some() {
Some(Heritage::new(ctx, &contexts))
} else {
None
};
if let Some((block_name, block_span)) = input.block {
let has_block = match &heritage {
Some(heritage) => heritage.blocks.contains_key(&block_name),
None => ctx.blocks.contains_key(&block_name),
};
if !has_block {
return Err(CompileError::no_file_info(
format_args!("cannot find block `{block_name}`"),
Some(block_span),
));
}
}
if input.print == Print::Ast || input.print == Print::All {
eprintln!("== Askama AST ==\n{:?}", templates[&input.path].nodes());
}
let size_hint = template_to_string(buf, &input, &contexts, heritage.as_ref(), tmpl_kind)?;
if input.print == Print::Code || input.print == Print::All {
eprintln!("== Askama code ==\n{}", buf.to_token_stream());
}
Ok(size_hint)
}
#[derive(Debug, Clone)]
struct CompileError {
msg: String,
span: Option<Span>,
}
impl CompileError {
fn new<S: fmt::Display>(msg: S, file_info: Option<FileInfo<'_>>) -> Self {
Self::new_with_span_stable(msg, file_info, None)
}
fn new_with_span<S: fmt::Display>(
msg: S,
file_info: Option<FileInfo<'_>>,
span: Option<Span>,
) -> Self {
let span = if let Some(span) = span
&& span.join(proc_macro2::Span::call_site()).is_none()
{
None
} else {
span
};
Self::new_with_span_stable(msg, file_info, span)
}
fn new_with_span_stable<S: fmt::Display>(
msg: S,
file_info: Option<FileInfo<'_>>,
span: Option<Span>,
) -> Self {
let msg = match (span, file_info) {
(None, Some(file_info)) => format!("{msg}{file_info}"),
_ => msg.to_string(),
};
Self { msg, span }
}
fn no_file_info<S: ToString>(msg: S, span: Option<Span>) -> Self {
Self {
msg: msg.to_string(),
span,
}
}
}
impl std::error::Error for CompileError {}
impl fmt::Display for CompileError {
#[inline]
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str(&self.msg)
}
}
#[derive(Debug, Clone, Copy)]
struct FileInfo<'a> {
path: &'a Path,
source: Option<&'a str>,
node_source: Option<&'a str>,
}
impl<'a> FileInfo<'a> {
fn new(path: &'a Path, source: Option<&'a str>, node_source: Option<&'a str>) -> Self {
Self {
path,
source,
node_source,
}
}
fn of(node: parser::Span, path: &'a Path, parsed: &'a Parsed) -> Self {
let source = parsed.source();
Self {
path,
source: Some(source),
node_source: node
.byte_range()
.and_then(|range| source.get(range.start..)),
}
}
}
impl fmt::Display for FileInfo<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !f.alternate() {
f.write_str("\n --> ")?;
}
if let (Some(source), Some(node_source)) = (self.source, self.node_source) {
let (error_info, file_path) = generate_error_info(source, node_source, self.path);
write!(
f,
"{file_path}:{row}:{column}",
row = error_info.row,
column = error_info.column,
)?;
if !f.alternate() {
write!(
f,
"\n{source_after}",
source_after = error_info.source_after,
)?;
}
Ok(())
} else {
write!(
f,
"{}",
match std::env::current_dir() {
Ok(cwd) => fmt_left!(move "{}", strip_common(&cwd, self.path)),
Err(_) => fmt_right!("{}", self.path.display()),
}
)
}
}
}
struct ErrorInfo {
row: usize,
column: usize,
source_after: String,
}
fn generate_row_and_column(src: &str, input: &str) -> ErrorInfo {
const MAX_LINE_LEN: usize = 80;
let offset = src.len() - input.len();
let (source_before, source_after) = src.split_at(offset);
let source_after = match source_after
.char_indices()
.enumerate()
.take(MAX_LINE_LEN + 1)
.last()
{
Some((MAX_LINE_LEN, (i, _))) => format!("{:?}...", &source_after[..i]),
_ => format!("{source_after:?}"),
};
let (row, last_line) = source_before.lines().enumerate().last().unwrap_or_default();
let column = last_line.chars().count();
ErrorInfo {
row: row + 1,
column,
source_after,
}
}
fn generate_error_info(src: &str, input: &str, file_path: &Path) -> (ErrorInfo, String) {
let file_path = match std::env::current_dir() {
Ok(cwd) => strip_common(&cwd, file_path),
Err(_) => file_path.display().to_string(),
};
let error_info = generate_row_and_column(src, input);
(error_info, file_path)
}
struct MsgValidEscapers<'a>(&'a [(Vec<Cow<'a, str>>, Cow<'a, str>)]);
impl fmt::Display for MsgValidEscapers<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut exts = self
.0
.iter()
.flat_map(|(exts, _)| exts)
.map(|x| format!("{x:?}"))
.collect::<Vec<_>>();
exts.sort();
write!(f, "The available extensions are: {}", exts.join(", "))
}
}
#[must_use = "Don't ignore the size_hint!"]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct SizeHint(usize);
impl SizeHint {
const EMPTY: Self = Self(0);
#[inline]
fn midpoint(self, rhs: Self) -> Self {
Self(self.0.midpoint(rhs.0))
}
}
impl std::ops::Add for SizeHint {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl<R> std::ops::AddAssign<R> for SizeHint
where
Self: std::ops::Add<R, Output = Self>,
{
#[inline]
fn add_assign(&mut self, rhs: R) {
*self = *self + rhs;
}
}
impl std::ops::Add<usize> for SizeHint {
type Output = Self;
#[inline]
fn add(self, rhs: usize) -> Self::Output {
Self(self.0 + rhs)
}
}
impl std::ops::Mul<usize> for SizeHint {
type Output = Self;
#[inline]
fn mul(self, rhs: usize) -> Self::Output {
Self(self.0 * rhs)
}
}
impl std::ops::Div<usize> for SizeHint {
type Output = Self;
#[inline]
fn div(self, rhs: usize) -> Self::Output {
Self(self.0 / rhs)
}
}
impl ToTokens for SizeHint {
#[inline]
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens);
}
#[inline]
fn to_token_stream(&self) -> TokenStream {
self.0.to_token_stream()
}
}
fn field_new(name: &str, span: proc_macro2::Span) -> TokenStream {
if name.starts_with(|c: char| c.is_ascii_digit()) {
let mut literal: Literal = name.parse().unwrap();
literal.set_span(span);
literal.into_token_stream()
} else if is_rust_keyword(name) && !matches!(name, "self" | "Self" | "crate" | "super") {
Ident::new_raw(name, span).into_token_stream()
} else {
Ident::new(name, span).into_token_stream()
}
}
fn var_writer() -> Ident {
syn::Ident::new("__askama_writer", proc_macro2::Span::call_site())
}
fn var_filter_source() -> Ident {
syn::Ident::new("__askama_filter_block", proc_macro2::Span::call_site())
}
fn var_let_source() -> Ident {
syn::Ident::new("__askama_let_block", proc_macro2::Span::call_site())
}
fn var_let_caller() -> Ident {
syn::Ident::new("__askama_let_caller", proc_macro2::Span::call_site())
}
fn var_values() -> Ident {
syn::Ident::new("__askama_values", proc_macro2::Span::call_site())
}
fn var_arg() -> Ident {
syn::Ident::new("__askama_arg", proc_macro2::Span::call_site())
}
fn var_item() -> Ident {
syn::Ident::new("__askama_item", proc_macro2::Span::call_site())
}
fn var_len() -> Ident {
syn::Ident::new("__askama_len", proc_macro2::Span::call_site())
}
fn var_iter() -> Ident {
syn::Ident::new("__askama_iter", proc_macro2::Span::call_site())
}
fn var_cycle() -> Ident {
syn::Ident::new("__askama_cycle", proc_macro2::Span::call_site())
}
fn var_did_loop() -> Ident {
syn::Ident::new("__askama_did_loop", proc_macro2::Span::call_site())
}
fn var_expr_n(n: usize, span: proc_macro2::Span) -> Ident {
syn::Ident::new(&format!("__askama_expr{n}"), span)
}
#[derive(Debug)]
struct OnceMap<K, V>([Mutex<HashMap<K, V>>; 8]);
impl<K, V> Default for OnceMap<K, V> {
fn default() -> Self {
Self(Default::default())
}
}
impl<K: Hash + Eq, V> OnceMap<K, V> {
fn get_or_try_insert<T, Q, E>(
&self,
key: &Q,
make_key_value: impl FnOnce(&Q) -> Result<(K, V), E>,
to_value: impl FnOnce(&V) -> T,
) -> Result<T, E>
where
K: Borrow<Q>,
Q: Hash + Eq,
{
let shard_idx = (FxBuildHasher.hash_one(key) % self.0.len() as u64) as usize;
let mut shard = self.0[shard_idx].lock().unwrap();
Ok(to_value(if let Some(v) = shard.get(key) {
v
} else {
let (k, v) = make_key_value(key)?;
match shard.entry(k) {
Entry::Vacant(entry) => entry.insert(v),
Entry::Occupied(_) => unreachable!("key in map when it should not have been"),
}
}))
}
}
enum EitherFormat<L, R>
where
L: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
R: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
{
Left(L),
Right(R),
}
impl<L, R> fmt::Display for EitherFormat<L, R>
where
L: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
R: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
{
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Left(v) => v(f),
Self::Right(v) => v(f),
}
}
}
macro_rules! fmt_left {
(move $fmt:literal $($tt:tt)*) => {
$crate::EitherFormat::Left(move |f: &mut std::fmt::Formatter<'_>| {
write!(f, $fmt $($tt)*)
})
};
($fmt:literal $($tt:tt)*) => {
$crate::EitherFormat::Left(|f: &mut std::fmt::Formatter<'_>| {
write!(f, $fmt $($tt)*)
})
};
}
macro_rules! fmt_right {
(move $fmt:literal $($tt:tt)*) => {
$crate::EitherFormat::Right(move |f: &mut std::fmt::Formatter<'_>| {
write!(f, $fmt $($tt)*)
})
};
($fmt:literal $($tt:tt)*) => {
$crate::EitherFormat::Right(|f: &mut std::fmt::Formatter<'_>| {
write!(f, $fmt $($tt)*)
})
};
}
macro_rules! quote_into {
($buffer:expr, $span:expr, { $($x:tt)+ } $(,)?) => {{
let buffer: &mut $crate::integration::Buffer = $buffer;
if !buffer.is_discard() {
let span: ::proc_macro2::Span = $span;
buffer.write_tokens(::quote::quote_spanned!(span => $($x)+));
}
}};
}
pub(crate) use fmt_left;
pub(crate) use fmt_right;
pub(crate) use quote_into;
type HashMap<K, V> = std::collections::hash_map::HashMap<K, V, FxBuildHasher>;
type HashSet<T> = std::collections::hash_set::HashSet<T, FxBuildHasher>;