Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add suppress_whitespace config option and add config parameter to the derive macro #664

Merged
merged 7 commits into from
Apr 21, 2022
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)]
GuillaumeGomez marked this conversation as resolved.
Show resolved Hide resolved
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());
}
djc marked this conversation as resolved.
Show resolved Hide resolved
} 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