Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 155 additions & 9 deletions askama_shared/src/generator.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::heritage::{Context, Heritage};
use crate::input::{Print, Source, TemplateInput};
use crate::parser::{parse, Cond, CondTest, Expr, Loop, Node, Target, When, Ws};
use crate::parser::{parse, Cond, CondTest, Expr, Loop, Node, Target, When, Whitespace, Ws};
use crate::{filters, get_template_source, read_config_file, CompileError, Config};

use proc_macro2::TokenStream;
Expand Down Expand Up @@ -28,9 +28,10 @@ pub fn derive_template(input: TokenStream) -> TokenStream {
/// the parse tree and/or generated source according to the `print` key's
/// value as passed to the `template()` attribute.
fn build_template(ast: &syn::DeriveInput) -> Result<String, CompileError> {
let config_toml = read_config_file()?;
let template_args = TemplateArgs::new(ast)?;
let config_toml = read_config_file(&template_args.config_path)?;
let config = Config::new(&config_toml)?;
let input = TemplateInput::new(ast, &config)?;
let input = TemplateInput::new(ast, &config, template_args)?;
let source: String = match input.source {
Source::Source(ref s) => s.clone(),
Source::Path(_) => get_template_source(&input.path)?,
Expand Down Expand Up @@ -60,14 +61,135 @@ fn build_template(ast: &syn::DeriveInput) -> Result<String, CompileError> {
eprintln!("{:?}", parsed[input.path.as_path()]);
}

let code = Generator::new(&input, &contexts, heritage.as_ref(), MapChain::new())
.build(&contexts[input.path.as_path()])?;
let code = Generator::new(
&input,
&contexts,
heritage.as_ref(),
MapChain::new(),
config.suppress_whitespace,
)
.build(&contexts[input.path.as_path()])?;
if input.print == Print::Code || input.print == Print::All {
eprintln!("{}", code);
}
Ok(code)
}

#[derive(Default)]
pub(crate) struct TemplateArgs {
pub(crate) source: Option<Source>,
pub(crate) print: Print,
pub(crate) escaping: Option<String>,
pub(crate) ext: Option<String>,
pub(crate) syntax: Option<String>,
pub(crate) config_path: Option<String>,
}

impl TemplateArgs {
fn new(ast: &'_ syn::DeriveInput) -> Result<Self, CompileError> {
// Check that an attribute called `template()` exists once and that it is
// the proper type (list).
let mut template_args = None;
for attr in &ast.attrs {
let ident = match attr.path.get_ident() {
Some(ident) => ident,
None => continue,
};

if ident == "template" {
if template_args.is_some() {
return Err("duplicated 'template' attribute".into());
}

match attr.parse_meta() {
Ok(syn::Meta::List(syn::MetaList { nested, .. })) => {
template_args = Some(nested);
}
Ok(_) => return Err("'template' attribute must be a list".into()),
Err(e) => return Err(format!("unable to parse attribute: {}", e).into()),
}
}
}
let template_args =
template_args.ok_or_else(|| CompileError::from("no attribute 'template' found"))?;

let mut args = Self::default();
// Loop over the meta attributes and find everything that we
// understand. Return a CompileError if something is not right.
// `source` contains an enum that can represent `path` or `source`.
for item in template_args {
let pair = match item {
syn::NestedMeta::Meta(syn::Meta::NameValue(ref pair)) => pair,
_ => {
return Err(format!(
"unsupported attribute argument {:?}",
item.to_token_stream()
)
.into())
}
};
let ident = match pair.path.get_ident() {
Some(ident) => ident,
None => unreachable!("not possible in syn::Meta::NameValue(…)"),
};

if ident == "path" {
if let syn::Lit::Str(ref s) = pair.lit {
if args.source.is_some() {
return Err("must specify 'source' or 'path', not both".into());
}
args.source = Some(Source::Path(s.value()));
} else {
return Err("template path must be string literal".into());
}
} else if ident == "source" {
if let syn::Lit::Str(ref s) = pair.lit {
if args.source.is_some() {
return Err("must specify 'source' or 'path', not both".into());
}
args.source = Some(Source::Source(s.value()));
} else {
return Err("template source must be string literal".into());
}
} else if ident == "print" {
if let syn::Lit::Str(ref s) = pair.lit {
args.print = s.value().parse()?;
} else {
return Err("print value must be string literal".into());
}
} else if ident == "escape" {
if let syn::Lit::Str(ref s) = pair.lit {
args.escaping = Some(s.value());
} else {
return Err("escape value must be string literal".into());
}
} else if ident == "ext" {
if let syn::Lit::Str(ref s) = pair.lit {
args.ext = Some(s.value());
} else {
return Err("ext value must be string literal".into());
}
} else if ident == "syntax" {
if let syn::Lit::Str(ref s) = pair.lit {
args.syntax = Some(s.value())
} else {
return Err("syntax value must be string literal".into());
}
} else if ident == "config" {
if let syn::Lit::Str(ref s) = pair.lit {
args.config_path = Some(s.value())
} else {
return Err("config value must be string literal".into());
}
} else {
return Err(format!("unsupported attribute key {:?} found", ident).into());
}
}

Ok(args)
}
}

fn find_used_templates(
input: &TemplateInput<'_>,
map: &mut HashMap<PathBuf, String>,
Expand Down Expand Up @@ -129,6 +251,8 @@ struct Generator<'a, S: std::hash::BuildHasher> {
buf_writable: Vec<Writable<'a>>,
// Counter for write! hash named arguments
named: usize,
// If set to `true`, the whitespace characters will be removed by default unless `+` is used.
suppress_whitespace: bool,
}

impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
Expand All @@ -137,6 +261,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
contexts: &'n HashMap<&'n Path, Context<'n>, S>,
heritage: Option<&'n Heritage<'_>>,
locals: MapChain<'n, &'n str, LocalMeta>,
suppress_whitespace: bool,
) -> Generator<'n, S> {
Generator {
input,
Expand All @@ -148,12 +273,19 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
super_block: None,
buf_writable: vec![],
named: 0,
suppress_whitespace,
}
}

fn child(&mut self) -> Generator<'_, S> {
let locals = MapChain::with_parent(&self.locals);
Self::new(self.input, self.contexts, self.heritage, locals)
Self::new(
self.input,
self.contexts,
self.heritage,
locals,
self.suppress_whitespace,
)
}

// Takes a Context and generates the relevant implementations.
Expand Down Expand Up @@ -222,7 +354,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
self.handle(ctx, ctx.nodes, buf, AstLevel::Top)
}?;

self.flush_ws(Ws(false, false));
self.flush_ws(Ws(None, None));
buf.writeln("::askama::Result::Ok(())")?;
buf.writeln("}")?;

Expand Down Expand Up @@ -1684,11 +1816,25 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
self.prepare_ws(ws);
}

fn should_trim_ws(&self, ws: Option<Whitespace>) -> bool {
match ws {
Some(Whitespace::Trim) => true,
Some(Whitespace::Preserve) => false,
None => self.suppress_whitespace,
}
}

// If the previous literal left some trailing whitespace in `next_ws` and the
// prefix whitespace suppressor from the given argument, flush that whitespace.
// In either case, `next_ws` is reset to `None` (no trailing whitespace).
fn flush_ws(&mut self, ws: Ws) {
if self.next_ws.is_some() && !ws.0 {
if self.next_ws.is_none() {
return;
}

// If `suppress_whitespace` is enabled, we keep the whitespace characters only if there is
// a `+` character.
if !self.should_trim_ws(ws.0) {
let val = self.next_ws.unwrap();
if !val.is_empty() {
self.buf_writable.push(Writable::Lit(val));
Expand All @@ -1701,7 +1847,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
// argument, to determine whether to suppress leading whitespace from the
// next literal.
fn prepare_ws(&mut self, ws: Ws) {
self.skip_ws = ws.1;
self.skip_ws = self.should_trim_ws(ws.1);
}
}

Expand Down
113 changes: 16 additions & 97 deletions askama_shared/src/input.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::generator::TemplateArgs;
use crate::{CompileError, Config, Syntax};

use std::path::{Path, PathBuf};
use std::str::FromStr;

use mime::Mime;
use quote::ToTokens;

pub(crate) struct TemplateInput<'a> {
pub(crate) ast: &'a syn::DeriveInput,
Expand All @@ -27,103 +27,16 @@ impl TemplateInput<'_> {
pub(crate) fn new<'n>(
ast: &'n syn::DeriveInput,
config: &'n Config<'_>,
args: TemplateArgs,
) -> Result<TemplateInput<'n>, CompileError> {
// Check that an attribute called `template()` exists once and that it is
// the proper type (list).
let mut template_args = None;
for attr in &ast.attrs {
let ident = match attr.path.get_ident() {
Some(ident) => ident,
None => continue,
};

if ident == "template" {
if template_args.is_some() {
return Err("duplicated 'template' attribute".into());
}

match attr.parse_meta() {
Ok(syn::Meta::List(syn::MetaList { nested, .. })) => {
template_args = Some(nested);
}
Ok(_) => return Err("'template' attribute must be a list".into()),
Err(e) => return Err(format!("unable to parse attribute: {}", e).into()),
}
}
}
let template_args =
template_args.ok_or_else(|| CompileError::from("no attribute 'template' found"))?;

// Loop over the meta attributes and find everything that we
// understand. Return a CompileError if something is not right.
// `source` contains an enum that can represent `path` or `source`.
let mut source = None;
let mut print = Print::None;
let mut escaping = None;
let mut ext = None;
let mut syntax = None;
for item in template_args {
let pair = match item {
syn::NestedMeta::Meta(syn::Meta::NameValue(ref pair)) => pair,
_ => {
return Err(format!(
"unsupported attribute argument {:?}",
item.to_token_stream()
)
.into())
}
};
let ident = match pair.path.get_ident() {
Some(ident) => ident,
None => unreachable!("not possible in syn::Meta::NameValue(…)"),
};

if ident == "path" {
if let syn::Lit::Str(ref s) = pair.lit {
if source.is_some() {
return Err("must specify 'source' or 'path', not both".into());
}
source = Some(Source::Path(s.value()));
} else {
return Err("template path must be string literal".into());
}
} else if ident == "source" {
if let syn::Lit::Str(ref s) = pair.lit {
if source.is_some() {
return Err("must specify 'source' or 'path', not both".into());
}
source = Some(Source::Source(s.value()));
} else {
return Err("template source must be string literal".into());
}
} else if ident == "print" {
if let syn::Lit::Str(ref s) = pair.lit {
print = s.value().parse()?;
} else {
return Err("print value must be string literal".into());
}
} else if ident == "escape" {
if let syn::Lit::Str(ref s) = pair.lit {
escaping = Some(s.value());
} else {
return Err("escape value must be string literal".into());
}
} else if ident == "ext" {
if let syn::Lit::Str(ref s) = pair.lit {
ext = Some(s.value());
} else {
return Err("ext value must be string literal".into());
}
} else if ident == "syntax" {
if let syn::Lit::Str(ref s) = pair.lit {
syntax = Some(s.value())
} else {
return Err("syntax value must be string literal".into());
}
} else {
return Err(format!("unsupported attribute key {:?} found", ident).into());
}
}
let TemplateArgs {
source,
print,
escaping,
ext,
syntax,
..
} = args;

// Validate the `source` and `ext` value together, since they are
// related. In case `source` was used instead of `path`, the value
Expand Down Expand Up @@ -261,6 +174,12 @@ impl FromStr for Print {
}
}

impl Default for Print {
fn default() -> Self {
Self::None
}
}

#[doc(hidden)]
pub fn extension_to_mime_type(ext: &str) -> Mime {
let basic_type = mime_guess::from_ext(ext).first_or_octet_stream();
Expand Down
Loading