diff --git a/rinja_derive/src/config.rs b/rinja_derive/src/config.rs index 3157cf85..1d4485f2 100644 --- a/rinja_derive/src/config.rs +++ b/rinja_derive/src/config.rs @@ -20,7 +20,7 @@ pub(crate) struct Config { pub(crate) syntaxes: BTreeMap>, pub(crate) default_syntax: &'static str, pub(crate) escapers: Vec<(Vec>, Cow<'static, str>)>, - pub(crate) whitespace: WhitespaceHandling, + pub(crate) whitespace: Whitespace, // `Config` is self referential and `_key` owns it data, so it must come last _key: OwnedConfigKey, } @@ -39,7 +39,7 @@ struct OwnedConfigKey(&'static ConfigKey<'static>); struct ConfigKey<'a> { source: Cow<'a, str>, config_path: Option>, - template_whitespace: Option>, + template_whitespace: Option, } impl<'a> ToOwned for ConfigKey<'a> { @@ -52,10 +52,7 @@ impl<'a> ToOwned for ConfigKey<'a> { .config_path .as_ref() .map(|s| Cow::Owned(s.as_ref().to_owned())), - template_whitespace: self - .template_whitespace - .as_ref() - .map(|s| Cow::Owned(s.as_ref().to_owned())), + template_whitespace: self.template_whitespace, }; OwnedConfigKey(Box::leak(Box::new(owned_key))) } @@ -72,7 +69,7 @@ impl Config { pub(crate) fn new( source: &str, config_path: Option<&str>, - template_whitespace: Option<&str>, + template_whitespace: Option, config_span: Option, ) -> Result<&'static Config, CompileError> { static CACHE: ManuallyDrop>> = @@ -81,7 +78,7 @@ impl Config { &ConfigKey { source: source.into(), config_path: config_path.map(Cow::Borrowed), - template_whitespace: template_whitespace.map(Cow::Borrowed), + template_whitespace, }, |key| { let config = Config::new_uncached(key.to_owned(), config_span)?; @@ -100,7 +97,6 @@ impl Config { ) -> Result { let s = key.0.source.as_ref(); let config_path = key.0.config_path.as_deref(); - let template_whitespace = key.0.template_whitespace.as_deref(); let root = manifest_root(); let default_dirs = vec![root.join("templates")]; @@ -114,7 +110,7 @@ impl Config { RawConfig::from_toml_str(s)? }; - let (dirs, default_syntax, mut whitespace) = match raw.general { + let (dirs, default_syntax, whitespace) = match raw.general { Some(General { dirs, default_syntax, @@ -126,26 +122,10 @@ impl Config { default_syntax.unwrap_or(DEFAULT_SYNTAX_NAME), whitespace, ), - None => ( - default_dirs, - DEFAULT_SYNTAX_NAME, - WhitespaceHandling::default(), - ), + None => (default_dirs, DEFAULT_SYNTAX_NAME, Whitespace::default()), }; let file_info = config_path.map(|path| FileInfo::new(Path::new(path), None, None)); - if let Some(template_whitespace) = template_whitespace { - whitespace = match template_whitespace { - "suppress" => WhitespaceHandling::Suppress, - "minimize" => WhitespaceHandling::Minimize, - "preserve" => WhitespaceHandling::Preserve, - s => { - return Err(CompileError::new( - format!("invalid value for `whitespace`: \"{s}\""), - file_info, - )); - } - }; - } + let whitespace = key.0.template_whitespace.unwrap_or(whitespace); if let Some(raw_syntaxes) = raw.syntax { for raw_s in raw_syntaxes { @@ -327,38 +307,13 @@ impl RawConfig<'_> { } } -#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash)] -#[cfg_attr(feature = "config", derive(Deserialize))] -#[cfg_attr(feature = "config", serde(field_identifier, rename_all = "lowercase"))] -pub(crate) enum WhitespaceHandling { - /// The default behavior. It will leave the whitespace characters "as is". - #[default] - Preserve, - /// It'll remove all the whitespace characters before and after the jinja block. - Suppress, - /// It'll remove all the whitespace characters except one before and after the jinja blocks. - /// If there is a newline character, the preserved character in the trimmed characters, it will - /// the one preserved. - Minimize, -} - -impl From for Whitespace { - fn from(ws: WhitespaceHandling) -> Self { - match ws { - WhitespaceHandling::Suppress => Whitespace::Suppress, - WhitespaceHandling::Preserve => Whitespace::Preserve, - WhitespaceHandling::Minimize => Whitespace::Minimize, - } - } -} - #[cfg_attr(feature = "config", derive(Deserialize))] struct General<'a> { #[cfg_attr(feature = "config", serde(borrow))] dirs: Option>, default_syntax: Option<&'a str>, #[cfg_attr(feature = "config", serde(default))] - whitespace: WhitespaceHandling, + whitespace: Whitespace, } #[cfg_attr(feature = "config", derive(Deserialize))] @@ -704,10 +659,10 @@ mod tests { None, ) .unwrap(); - assert_eq!(config.whitespace, WhitespaceHandling::Suppress); + assert_eq!(config.whitespace, Whitespace::Suppress); let config = Config::new(r#""#, None, None, None).unwrap(); - assert_eq!(config.whitespace, WhitespaceHandling::Preserve); + assert_eq!(config.whitespace, Whitespace::Preserve); let config = Config::new( r#" @@ -719,7 +674,7 @@ mod tests { None, ) .unwrap(); - assert_eq!(config.whitespace, WhitespaceHandling::Preserve); + assert_eq!(config.whitespace, Whitespace::Preserve); let config = Config::new( r#" @@ -731,7 +686,7 @@ mod tests { None, ) .unwrap(); - assert_eq!(config.whitespace, WhitespaceHandling::Minimize); + assert_eq!(config.whitespace, Whitespace::Minimize); } #[cfg(feature = "config")] @@ -739,30 +694,20 @@ mod tests { fn test_whitespace_in_template() { // Checking that template arguments have precedence over general configuration. // So in here, in the template arguments, there is `whitespace = "minimize"` so - // the `WhitespaceHandling` should be `Minimize` as well. + // the `Whitespace` should be `Minimize` as well. let config = Config::new( r#" [general] whitespace = "suppress" "#, None, - Some("minimize"), + Some(Whitespace::Minimize), None, ) .unwrap(); - assert_eq!(config.whitespace, WhitespaceHandling::Minimize); + assert_eq!(config.whitespace, Whitespace::Minimize); - let config = Config::new(r#""#, None, Some("minimize"), None).unwrap(); - assert_eq!(config.whitespace, WhitespaceHandling::Minimize); - } - - #[test] - fn test_config_whitespace_error() { - let config = Config::new(r"", None, Some("trim"), None); - if let Err(err) = config { - assert_eq!(err.msg, "invalid value for `whitespace`: \"trim\""); - } else { - panic!("Config::new should have return an error"); - } + let config = Config::new(r#""#, None, Some(Whitespace::Minimize), None).unwrap(); + assert_eq!(config.whitespace, Whitespace::Minimize); } } diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index 2a12805e..2fa7d039 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -15,7 +15,6 @@ use parser::{ }; use rustc_hash::FxBuildHasher; -use crate::config::WhitespaceHandling; use crate::heritage::{Context, Heritage}; use crate::html::write_escaped_str; use crate::input::{Source, TemplateInput}; @@ -72,7 +71,7 @@ struct Generator<'a, 'h> { next_ws: Option<&'a str>, // Whitespace suppression from the previous non-literal. Will be used to // determine whether to flush prefix whitespace from the next literal. - skip_ws: WhitespaceHandling, + skip_ws: Whitespace, // If currently in a block, this will contain the name of a potential parent block super_block: Option<(&'a str, usize)>, // Buffer for writable @@ -96,7 +95,7 @@ impl<'a, 'h> Generator<'a, 'h> { heritage, locals, next_ws: None, - skip_ws: WhitespaceHandling::Preserve, + skip_ws: Whitespace::Preserve, super_block: None, buf_writable: WritableBuffer { discard: buf_writable_discard, @@ -328,7 +327,7 @@ impl<'a, 'h> Generator<'a, 'h> { if AstLevel::Top == level { // Handle any pending whitespace. if self.next_ws.is_some() { - self.flush_ws(Ws(Some(self.skip_ws.into()), None)); + self.flush_ws(Ws(Some(self.skip_ws), None)); } size_hint += self.write_buf_writable(ctx, buf)?; @@ -758,20 +757,22 @@ impl<'a, 'h> Generator<'a, 'h> { let (def, own_ctx) = if let Some(s) = scope { let path = ctx.imports.get(s).ok_or_else(|| { - ctx.generate_error(&format!("no import found for scope {s:?}"), call) + ctx.generate_error(format_args!("no import found for scope {s:?}"), call) })?; let mctx = self.contexts.get(path).ok_or_else(|| { - ctx.generate_error(&format!("context for {path:?} not found"), call) + ctx.generate_error(format_args!("context for {path:?} not found"), call) })?; let def = mctx.macros.get(name).ok_or_else(|| { - ctx.generate_error(&format!("macro {name:?} not found in scope {s:?}"), call) + ctx.generate_error( + format_args!("macro {name:?} not found in scope {s:?}"), + call, + ) })?; (def, mctx) } else { - let def = ctx - .macros - .get(name) - .ok_or_else(|| ctx.generate_error(&format!("macro {name:?} not found"), call))?; + let def = ctx.macros.get(name).ok_or_else(|| { + ctx.generate_error(format_args!("macro {name:?} not found"), call) + })?; (def, ctx) }; @@ -794,7 +795,7 @@ impl<'a, 'h> Generator<'a, 'h> { }; if !def.args.iter().any(|(arg, _)| arg == arg_name) { return Err(ctx.generate_error( - &format!("no argument named `{arg_name}` in macro {name:?}"), + format_args!("no argument named `{arg_name}` in macro {name:?}"), call, )); } @@ -825,7 +826,7 @@ impl<'a, 'h> Generator<'a, 'h> { // to use unnamed ones at this point anymore. if !allow_positional { return Err(ctx.generate_error( - &format!( + format_args!( "cannot have unnamed argument (`{arg}`) after named argument \ in call to macro {name:?}" ), @@ -837,7 +838,7 @@ impl<'a, 'h> Generator<'a, 'h> { Some(arg_expr) if used_named_args[index] => { let Expr::NamedArgument(name, _) = **arg_expr else { unreachable!() }; return Err(ctx.generate_error( - &format!("`{name}` is passed more than once"), + format_args!("`{name}` is passed more than once"), call, )); } @@ -845,7 +846,7 @@ impl<'a, 'h> Generator<'a, 'h> { if let Some(default_value) = default_value { default_value } else { - return Err(ctx.generate_error(&format!("missing `{arg}` argument"), call)); + return Err(ctx.generate_error(format_args!("missing `{arg}` argument"), call)); } } } @@ -1129,7 +1130,7 @@ impl<'a, 'h> Generator<'a, 'h> { // A block definition contains a block definition of the same name (Some(cur_name), Some((prev_name, _))) if cur_name == prev_name => { return Err(ctx.generate_error( - &format!("cannot define recursive blocks ({cur_name})"), + format_args!("cannot define recursive blocks ({cur_name})"), node, )); } @@ -1325,15 +1326,15 @@ impl<'a, 'h> Generator<'a, 'h> { let Lit { lws, val, rws } = *lit; if !lws.is_empty() { match self.skip_ws { - WhitespaceHandling::Suppress => {} + Whitespace::Suppress => {} _ if val.is_empty() => { assert!(rws.is_empty()); self.next_ws = Some(lws); } - WhitespaceHandling::Preserve => { + Whitespace::Preserve => { self.buf_writable.push(Writable::Lit(Cow::Borrowed(lws))); } - WhitespaceHandling::Minimize => { + Whitespace::Minimize => { self.buf_writable.push(Writable::Lit(Cow::Borrowed( match lws.contains('\n') { true => "\n", @@ -1345,7 +1346,7 @@ impl<'a, 'h> Generator<'a, 'h> { } if !val.is_empty() { - self.skip_ws = WhitespaceHandling::Preserve; + self.skip_ws = Whitespace::Preserve; self.buf_writable.push(Writable::Lit(Cow::Borrowed(val))); } @@ -1577,7 +1578,7 @@ impl<'a, 'h> Generator<'a, 'h> { ) -> Result { if cfg!(not(feature = "urlencode")) { return Err(ctx.generate_error( - &format!("the `{name}` filter requires the `urlencode` feature to be enabled"), + format_args!("the `{name}` filter requires the `urlencode` feature to be enabled"), node, )); } @@ -1666,9 +1667,10 @@ impl<'a, 'h> Generator<'a, 'h> { node: &WithSpan<'_, T>, ) -> Result { if args.len() != 1 { - return Err( - ctx.generate_error(&format!("unexpected argument(s) in `{name}` filter"), node) - ); + return Err(ctx.generate_error( + format_args!("unexpected argument(s) in `{name}` filter"), + node, + )); } buf.write(format_args!( "rinja::filters::{name}(&(&&rinja::filters::AutoEscaper::new(&(", @@ -1778,7 +1780,7 @@ impl<'a, 'h> Generator<'a, 'h> { "CStr" }; return Err(ctx.generate_error( - &format!( + format_args!( "invalid escaper `b{content:?}`. Expected a string, found a {kind}" ), &args[1], @@ -1804,7 +1806,7 @@ impl<'a, 'h> Generator<'a, 'h> { }) .ok_or_else(|| { ctx.generate_error( - &format!( + format_args!( "invalid escaper '{name}' for `escape` filter. {}", MsgValidEscapers(&self.input.config.escapers), ), @@ -2053,7 +2055,11 @@ impl<'a, 'h> Generator<'a, 'h> { ); } }, - s => return Err(ctx.generate_error(&format!("unknown loop method: {s:?}"), left)), + s => { + return Err( + ctx.generate_error(format_args!("unknown loop method: {s:?}"), left) + ); + } }, sub_left => { match sub_left { @@ -2342,13 +2348,8 @@ impl<'a, 'h> Generator<'a, 'h> { self.prepare_ws(ws); } - fn should_trim_ws(&self, ws: Option) -> WhitespaceHandling { - match ws { - Some(Whitespace::Suppress) => WhitespaceHandling::Suppress, - Some(Whitespace::Preserve) => WhitespaceHandling::Preserve, - Some(Whitespace::Minimize) => WhitespaceHandling::Minimize, - None => self.input.config.whitespace, - } + fn should_trim_ws(&self, ws: Option) -> Whitespace { + ws.unwrap_or(self.input.config.whitespace) } // If the previous literal left some trailing whitespace in `next_ws` and the @@ -2362,13 +2363,13 @@ impl<'a, 'h> Generator<'a, 'h> { // If `whitespace` is set to `suppress`, we keep the whitespace characters only if there is // a `+` character. match self.should_trim_ws(ws.0) { - WhitespaceHandling::Preserve => { + Whitespace::Preserve => { let val = self.next_ws.unwrap(); if !val.is_empty() { self.buf_writable.push(Writable::Lit(Cow::Borrowed(val))); } } - WhitespaceHandling::Minimize => { + Whitespace::Minimize => { let val = self.next_ws.unwrap(); if !val.is_empty() { self.buf_writable.push(Writable::Lit(Cow::Borrowed( @@ -2379,7 +2380,7 @@ impl<'a, 'h> Generator<'a, 'h> { ))); } } - WhitespaceHandling::Suppress => {} + Whitespace::Suppress => {} } self.next_ws = None; } @@ -2419,7 +2420,7 @@ fn macro_call_ensure_arg_count( _ => (nb_default_args, "at least "), }; Err(ctx.generate_error( - &format!( + format_args!( "macro {:?} expected {extra}{expected_args} argument{}, found {}", def.name, if expected_args != 1 { "s" } else { "" }, diff --git a/rinja_derive/src/heritage.rs b/rinja_derive/src/heritage.rs index be65ff1b..12e3c229 100644 --- a/rinja_derive/src/heritage.rs +++ b/rinja_derive/src/heritage.rs @@ -1,3 +1,4 @@ +use core::fmt; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; @@ -140,7 +141,11 @@ impl Context<'_> { }) } - pub(crate) fn generate_error(&self, msg: &str, node: &WithSpan<'_, T>) -> CompileError { + pub(crate) fn generate_error( + &self, + msg: impl fmt::Display, + node: &WithSpan<'_, T>, + ) -> CompileError { CompileError::new( msg, self.path.map(|path| FileInfo::of(node, path, self.parsed)), diff --git a/rinja_derive/src/input.rs b/rinja_derive/src/input.rs index bccd3efa..ee2ee628 100644 --- a/rinja_derive/src/input.rs +++ b/rinja_derive/src/input.rs @@ -7,6 +7,7 @@ use std::str::FromStr; use std::sync::{Arc, OnceLock}; use mime::Mime; +use parser::node::Whitespace; use parser::{Node, Parsed}; use proc_macro2::Span; use rustc_hash::FxBuildHasher; @@ -282,7 +283,7 @@ pub(crate) struct TemplateArgs { ext_span: Option, syntax: Option, config: Option, - pub(crate) whitespace: Option, + pub(crate) whitespace: Option, pub(crate) template_span: Option, pub(crate) config_span: Option, } @@ -307,15 +308,15 @@ impl TemplateArgs { }; Ok(Self { source: match args.source { - Some((_, PartialTemplateArgsSource::Path(s))) => { + Some(PartialTemplateArgsSource::Path(s)) => { (Source::Path(s.value()), Some(s.span())) } - Some((_, PartialTemplateArgsSource::Source(s))) => { + Some(PartialTemplateArgsSource::Source(s)) => { (Source::Source(s.value().into()), Some(s.span())) } #[cfg(feature = "code-in-doc")] - Some((ident, PartialTemplateArgsSource::InDoc(_))) => { - source_from_docs(&ident, &args.meta_docs, ast)? + Some(PartialTemplateArgsSource::InDoc(s)) => { + source_from_docs(s.span(), &args.meta_docs, ast)? } None => { return Err(CompileError::no_file_info( @@ -327,16 +328,16 @@ impl TemplateArgs { )); } }, - block: args.block.map(|(_, value)| value.value()), - print: args.print.map(|(_, _, value)| value).unwrap_or_default(), - escaping: args.escape.map(|(_, value)| value.value()), - ext: args.ext.as_ref().map(|(_, s)| s.value()), - ext_span: args.ext.as_ref().map(|(_, s)| s.span()), - syntax: args.syntax.map(|(_, value)| value.value()), - config: args.config.as_ref().map(|(_, s)| s.value()), - whitespace: args.whitespace.map(|(_, value)| value.value()), + block: args.block.map(|value| value.value()), + print: args.print.unwrap_or_default(), + escaping: args.escape.map(|value| value.value()), + ext: args.ext.as_ref().map(|value| value.value()), + ext_span: args.ext.as_ref().map(|value| value.span()), + syntax: args.syntax.map(|value| value.value()), + config: args.config.as_ref().map(|value| value.value()), + whitespace: args.whitespace, template_span: Some(template.span()), - config_span: args.config.as_ref().map(|(_, s)| s.span()), + config_span: args.config.as_ref().map(|value| value.span()), }) } @@ -366,33 +367,33 @@ impl TemplateArgs { /// This is only done if no path or source was given in the `#[template]` attribute. #[cfg(feature = "code-in-doc")] fn source_from_docs( - name: &Ident, + span: Span, docs: &[Attribute], ast: &syn::DeriveInput, ) -> Result<(Source, Option), CompileError> { - let (span, source) = collect_comment_blocks(name, docs, ast)?; + let (source_span, source) = collect_comment_blocks(span, docs, ast)?; let source = strip_common_ws_prefix(source); - let source = collect_rinja_code_blocks(name, ast, source)?; - Ok((source, span)) + let source = collect_rinja_code_blocks(span, ast, source)?; + Ok((source, source_span)) } #[cfg(feature = "code-in-doc")] fn collect_comment_blocks( - name: &Ident, + span: Span, docs: &[Attribute], ast: &syn::DeriveInput, ) -> Result<(Option, String), CompileError> { - let mut span: Option = None; + let mut source_span: Option = None; let mut assign_span = |kv: &syn::MetaNameValue| { // FIXME: uncomment once is stable // let new_span = kv.path.span(); - // span = Some(match span { + // source_span = Some(match source_span { // Some(cur_span) => cur_span.join(new_span).unwrap_or(cur_span), // None => new_span, // }); - if span.is_none() { - span = Some(kv.path.span()); + if source_span.is_none() { + source_span = Some(kv.path.span()); } }; @@ -424,14 +425,14 @@ fn collect_comment_blocks( source.push('\n'); } if source.is_empty() { - return Err(no_rinja_code_block(name, ast)); + return Err(no_rinja_code_block(span, ast)); } - Ok((span, source)) + Ok((source_span, source)) } #[cfg(feature = "code-in-doc")] -fn no_rinja_code_block(name: &Ident, ast: &syn::DeriveInput) -> CompileError { +fn no_rinja_code_block(span: Span, ast: &syn::DeriveInput) -> CompileError { let kind = match &ast.data { syn::Data::Struct(_) => "struct", syn::Data::Enum(_) => "enum", @@ -440,9 +441,10 @@ fn no_rinja_code_block(name: &Ident, ast: &syn::DeriveInput) -> CompileError { }; CompileError::no_file_info( format!( - "when using `in_doc = true`, the {kind}'s documentation needs a `rinja` code block" + "when using `in_doc` with the value `true`, the {kind}'s documentation needs a \ + `rinja` code block" ), - Some(name.span()), + Some(span), ) } @@ -476,7 +478,7 @@ fn strip_common_ws_prefix(source: String) -> String { #[cfg(feature = "code-in-doc")] fn collect_rinja_code_blocks( - name: &Ident, + span: Span, ast: &syn::DeriveInput, source: String, ) -> Result { @@ -499,7 +501,7 @@ fn collect_rinja_code_blocks( } } if !had_rinja_code { - return Err(no_rinja_code_block(name, ast)); + return Err(no_rinja_code_block(span, ast)); } if tmpl_source.ends_with('\n') { @@ -651,14 +653,14 @@ pub(crate) fn get_template_source( pub(crate) struct PartialTemplateArgs { pub(crate) template: Option, pub(crate) meta_docs: Vec, - pub(crate) source: Option<(Ident, PartialTemplateArgsSource)>, - pub(crate) block: Option<(Ident, LitStr)>, - pub(crate) print: Option<(Ident, LitStr, Print)>, - pub(crate) escape: Option<(Ident, LitStr)>, - pub(crate) ext: Option<(Ident, LitStr)>, - pub(crate) syntax: Option<(Ident, LitStr)>, - pub(crate) config: Option<(Ident, LitStr)>, - pub(crate) whitespace: Option<(Ident, LitStr)>, + pub(crate) source: Option, + pub(crate) block: Option, + pub(crate) print: Option, + pub(crate) escape: Option, + pub(crate) ext: Option, + pub(crate) syntax: Option, + pub(crate) config: Option, + pub(crate) whitespace: Option, } pub(crate) enum PartialTemplateArgsSource { @@ -719,12 +721,11 @@ const _: () = { if ident == "path" { ensure_source_only_once(ident, &this.source)?; - let value = get_strlit(ident, value)?; - this.source = Some((ident.clone(), PartialTemplateArgsSource::Path(value))); + this.source = Some(PartialTemplateArgsSource::Path(get_strlit(ident, value)?)); } else if ident == "source" { ensure_source_only_once(ident, &this.source)?; - let value = get_strlit(ident, value)?; - this.source = Some((ident.clone(), PartialTemplateArgsSource::Source(value))); + this.source = + Some(PartialTemplateArgsSource::Source(get_strlit(ident, value)?)); } else if ident == "in_doc" { let value = get_boollit(ident, value)?; if !value.value() { @@ -741,19 +742,12 @@ const _: () = { } #[cfg(feature = "code-in-doc")] { - this.source = - Some((ident.clone(), PartialTemplateArgsSource::InDoc(value))); + this.source = Some(PartialTemplateArgsSource::InDoc(value)); } } else if ident == "block" { set_strlit_pair(ident, value, &mut this.block)?; } else if ident == "print" { - ensure_only_once(ident, &mut this.print)?; - let str_value = get_strlit(ident, value)?; - let value = str_value - .value() - .parse() - .map_err(|msg| CompileError::no_file_info(msg, Some(ident.span())))?; - this.print = Some((ident.clone(), str_value, value)); + set_parseable_string(ident, value, &mut this.print)?; } else if ident == "escape" { set_strlit_pair(ident, value, &mut this.escape)?; } else if ident == "ext" { @@ -763,7 +757,7 @@ const _: () = { } else if ident == "config" { set_strlit_pair(ident, value, &mut this.config)?; } else if ident == "whitespace" { - set_strlit_pair(ident, value, &mut this.whitespace)?; + set_parseable_string(ident, value, &mut this.whitespace)?; } else { return Err(CompileError::no_file_info( format!("unsupported template attribute `{ident}` found"), @@ -778,11 +772,26 @@ const _: () = { fn set_strlit_pair( name: &Ident, value: ExprLit, - dest: &mut Option<(Ident, LitStr)>, + dest: &mut Option, ) -> Result<(), CompileError> { ensure_only_once(name, dest)?; - let value = get_strlit(name, value)?; - *dest = Some((name.clone(), value)); + *dest = Some(get_strlit(name, value)?); + Ok(()) + } + + fn set_parseable_string>( + name: &Ident, + value: ExprLit, + dest: &mut Option, + ) -> Result<(), CompileError> { + ensure_only_once(name, dest)?; + let str_value = get_strlit(name, value)?; + *dest = Some( + str_value + .value() + .parse() + .map_err(|msg| CompileError::no_file_info(msg, Some(str_value.span())))?, + ); Ok(()) } @@ -836,7 +845,7 @@ const _: () = { fn ensure_source_only_once( name: &Ident, - source: &Option<(Ident, PartialTemplateArgsSource)>, + source: &Option, ) -> Result<(), CompileError> { if source.is_some() { return Err(CompileError::no_file_info( diff --git a/rinja_derive/src/lib.rs b/rinja_derive/src/lib.rs index 21dad0e2..48de87c8 100644 --- a/rinja_derive/src/lib.rs +++ b/rinja_derive/src/lib.rs @@ -128,11 +128,7 @@ pub fn derive_template(input: TokenStream12) -> TokenStream12 { }; match build_template(&ast) { Ok(source) => source.parse().unwrap(), - Err(CompileError { - msg, - span, - rendered: _rendered, - }) => { + Err(CompileError { msg, span }) => { let mut ts = compile_error(std::iter::once(msg), span.unwrap_or(ast.ident.span())); if let Ok(source) = build_skeleton(&ast) { let source: TokenStream = source.parse().unwrap(); @@ -191,7 +187,7 @@ fn build_template_inner( let config = Config::new( &s, config_path, - template_args.whitespace.as_deref(), + template_args.whitespace, template_args.config_span, )?; let input = TemplateInput::new(ast, config, template_args)?; @@ -237,7 +233,6 @@ fn build_template_inner( struct CompileError { msg: String, span: Option, - rendered: bool, } impl CompileError { @@ -254,18 +249,13 @@ impl CompileError { Some(file_info) => format!("{msg}{file_info}"), None => msg.to_string(), }; - Self { - msg, - span, - rendered: false, - } + Self { msg, span } } fn no_file_info(msg: S, span: Option) -> Self { Self { msg: msg.to_string(), span, - rendered: false, } } } diff --git a/rinja_derive/src/tests.rs b/rinja_derive/src/tests.rs index 8abb8ec3..b83a477b 100644 --- a/rinja_derive/src/tests.rs +++ b/rinja_derive/src/tests.rs @@ -747,7 +747,8 @@ fn test_code_in_comment() { let err = build_template(&ast).unwrap_err(); assert_eq!( err.to_string(), - "when using `in_doc = true`, the struct's documentation needs a `rinja` code block" + "when using `in_doc` with the value `true`, the struct's documentation needs a `rinja` \ + code block" ); let ts = " diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index 8daf2aeb..d797e38d 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -1,5 +1,5 @@ use std::collections::HashSet; -use std::str; +use std::str::{self, FromStr}; use winnow::Parser; use winnow::combinator::{ @@ -426,8 +426,11 @@ impl<'a> CondTest<'a> { } } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash)] +#[cfg_attr(feature = "config", derive(serde::Deserialize))] +#[cfg_attr(feature = "config", serde(field_identifier, rename_all = "lowercase"))] pub enum Whitespace { + #[default] Preserve, Suppress, Minimize, @@ -448,6 +451,19 @@ impl Whitespace { } } +impl FromStr for Whitespace { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "+" | "preserve" => Ok(Whitespace::Preserve), + "-" | "suppress" => Ok(Whitespace::Suppress), + "~" | "minimize" => Ok(Whitespace::Minimize), + s => Err(format!("invalid value for `whitespace`: {s:?}")), + } + } +} + fn check_block_start<'a>( i: &'a str, start: &'a str, diff --git a/testing/tests/ui/rinja-block.stderr b/testing/tests/ui/rinja-block.stderr index eef4e97a..9851374e 100644 --- a/testing/tests/ui/rinja-block.stderr +++ b/testing/tests/ui/rinja-block.stderr @@ -6,11 +6,11 @@ error: unknown node `fail` 5 | /// Some documentation | ^^^^^^^^^^^^^^^^^^^^^^ -error: when using `in_doc = true`, the struct's documentation needs a `rinja` code block - --> tests/ui/rinja-block.rs:15:25 +error: when using `in_doc` with the value `true`, the struct's documentation needs a `rinja` code block + --> tests/ui/rinja-block.rs:15:34 | 15 | #[template(ext = "txt", in_doc = true)] - | ^^^^^^ + | ^^^^ error: specify one template argument `path`, `source` or `in_doc` --> tests/ui/rinja-block.rs:24:3 @@ -36,11 +36,11 @@ error: template attribute `in_doc` expects a boolean value 51 | #[template(ext = "txt", in_doc = "yes")] | ^^^^^ -error: when using `in_doc = true`, the enum's documentation needs a `rinja` code block - --> tests/ui/rinja-block.rs:60:25 +error: when using `in_doc` with the value `true`, the enum's documentation needs a `rinja` code block + --> tests/ui/rinja-block.rs:60:34 | 60 | #[template(ext = "txt", in_doc = true)] - | ^^^^^^ + | ^^^^ error: rinja templates are not supported for `union` types, only `struct` and `enum` --> tests/ui/rinja-block.rs:68:1