Skip to content

Commit

Permalink
feat: combine static strings in concats into the format string for sm…
Browse files Browse the repository at this point in the history
…aller templates
  • Loading branch information
0b10011 committed Jan 9, 2025
1 parent fb15b38 commit 23d7c75
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 40 deletions.
98 changes: 70 additions & 28 deletions oxiplate-derive/src/syntax/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use nom::bytes::complete::{tag, take, take_while, take_while1};
use nom::character::complete::char;
use nom::combinator::{cut, fail, not, opt, peek};
use nom::error::context;
use nom::multi::{many0, many_till};
use nom::multi::{many0, many1, many_till};
use nom::sequence::{pair, tuple};
use proc_macro2::{Group, TokenStream};
use quote::{quote, quote_spanned, ToTokens, TokenStreamExt};
Expand Down Expand Up @@ -153,11 +153,7 @@ pub(crate) enum Expression<'a> {
Number(Source<'a>),
Bool(bool, Source<'a>),
// Group(Box<Expression<'a>>),
Concat(
Box<ExpressionAccess<'a>>,
Source<'a>,
Box<ExpressionAccess<'a>>,
),
Concat(Vec<ExpressionAccess<'a>>, Source<'a>),
Calc(
Box<ExpressionAccess<'a>>,
Operator<'a>,
Expand Down Expand Up @@ -196,9 +192,39 @@ impl ToTokens for Expression<'_> {
}
}
},
Expression::Concat(left, source, right) => {
let span = source.span();
quote_spanned! {span=> format!("{}{}", #left, #right) }
Expression::Concat(expressions, concat_operator) => {
let span = concat_operator.span();

let mut format_tokens = vec![];
let mut argument_tokens = vec![];
for expression in expressions {
match expression {
ExpressionAccess {
expression: Expression::String(string),
fields,
} if fields.is_empty() => {
let string = syn::LitStr::new(string.as_str(), string.span());
format_tokens.push(quote_spanned! {span=> #string });
}
_ => {
format_tokens.push(quote_spanned! {span=> "{}" });
argument_tokens.push(quote!(#expression));
}
}
}

if argument_tokens.is_empty() {
return;
}

let format_concat_tokens = quote! { concat!(#(#format_tokens),*) };
format_tokens.clear();

if argument_tokens.is_empty() {
format_concat_tokens
} else {
quote_spanned! {span=> format!(#format_concat_tokens, #(#argument_tokens),*) }
}
}
Expression::Calc(left, operator, right) => quote!(#left #operator #right),
Expression::Prefixed(operator, expression) => quote!(#operator #expression),
Expand Down Expand Up @@ -340,13 +366,14 @@ impl ToTokens for PrefixOperator<'_> {

pub(super) fn expression<'a>(
state: &'a State,
allow_recursion: bool,
allow_calc: bool,
allow_concat: bool,
) -> impl Fn(Source) -> Res<Source, ExpressionAccess> + 'a {
move |input| {
let (input, (expression, fields)) = pair(
alt((
concat(state, allow_recursion),
calc(state, allow_recursion),
concat(state, allow_concat),
calc(state, allow_calc),
string,
number,
bool,
Expand Down Expand Up @@ -485,44 +512,59 @@ fn string(input: Source) -> Res<Source, Expression> {
}
fn concat<'a>(
state: &'a State,
allow_recursion: bool,
allow_concat: bool,
) -> impl Fn(Source) -> Res<Source, Expression> + 'a {
move |input| {
if !allow_recursion {
if !allow_concat {
return fail(input);
}
let (input, (left, _leading_whitespace, tilde, _trailing_whitespace, right)) =
tuple((
expression(state, false),
let (input, (left, concats)) = tuple((
expression(state, false, false),
many1(tuple((
opt(whitespace),
tag("~"),
opt(whitespace),
context("Expected an expression", cut(expression(state, true))),
))(input)?;
context(
"Expected an expression",
cut(expression(state, true, false)),
),
))),
))(input)?;
let mut expressions = vec![left];
let mut tilde_operator = None;
for (_leading_whitespace, tilde, _trailing_whitespace, expression) in concats {
if tilde_operator.is_none() {
tilde_operator = Some(tilde);
}
expressions.push(expression);
}
Ok((
input,
Expression::Concat(Box::new(left), tilde, Box::new(right)),
Expression::Concat(
expressions,
tilde_operator.expect("Tilde should be guaranteed here"),
),
))
}
}

fn calc<'a>(
state: &'a State,
allow_recursion: bool,
) -> impl Fn(Source) -> Res<Source, Expression> + 'a {
fn calc<'a>(state: &'a State, allow_calc: bool) -> impl Fn(Source) -> Res<Source, Expression> + 'a {
move |input| {
if !allow_recursion {
if !allow_calc {
return fail(input);
}
let (input, (left, _leading_whitespace, (), operator, _trailing_whitespace, right)) =
tuple((
expression(state, false),
expression(state, false, false),
opt(whitespace),
// End tags like `-}}` and `%}` could be matched by operator; this ensures we can use `cut()` later.
not(alt((tag_end("}}"), tag_end("%}"), tag_end("#}")))),
operator,
opt(whitespace),
context("Expected an expression", cut(expression(state, true))),
context(
"Expected an expression",
cut(expression(state, true, false)),
),
))(input)?;
Ok((
input,
Expand All @@ -547,7 +589,7 @@ fn prefixed_expression<'a>(state: &'a State) -> impl Fn(Source) -> Res<Source, E
prefix_operator,
context(
"Expected an expression after prefix operator",
cut(expression(state, true)),
cut(expression(state, true, true)),
),
))(input)?;

Expand Down
2 changes: 1 addition & 1 deletion oxiplate-derive/src/syntax/statement/for.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ pub(super) fn parse_for<'a>(state: &'a State) -> impl Fn(Source) -> Res<Source,
context("Expected space after 'in'", take_while1(is_whitespace)),
context(
"Expected an expression that is iterable",
expression(state, true),
expression(state, true, true),
),
)))(input)?;

Expand Down
6 changes: 3 additions & 3 deletions oxiplate-derive/src/syntax/statement/if.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ fn parse_if_generic<'a>(state: &'a State) -> impl FnMut(Source) -> Res<Source, I
&ws0,
context(
"Expected an expression after `=`",
cut(expression(state, true)),
cut(expression(state, true, true)),
),
),
),
Expand All @@ -274,7 +274,7 @@ fn parse_if_generic<'a>(state: &'a State) -> impl FnMut(Source) -> Res<Source, I
&ws0,
context(
"Expected an expression after `=`",
cut(expression(state, true)),
cut(expression(state, true, true)),
),
),
),
Expand All @@ -285,7 +285,7 @@ fn parse_if_generic<'a>(state: &'a State) -> impl FnMut(Source) -> Res<Source, I
} else {
let (input, output) = context(
"Expected an expression after `if`",
cut(expression(state, true)),
cut(expression(state, true, true)),
)(input)?;
Ok((input, IfType::If(output)))
}
Expand Down
6 changes: 4 additions & 2 deletions oxiplate-derive/src/syntax/writ.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,10 @@ pub(super) fn writ<'a>(
} else {
Escaper::default(state)
};
let (input, output) =
context("Expected an expression.", cut(expression(state, true)))(input)?;
let (input, output) = context(
"Expected an expression.",
cut(expression(state, true, true)),
)(input)?;
let (input, trailing_whitespace) = context(
"Expecting the writ tag to be closed with `_}}`, `-}}`, or `}}`.",
cut(preceded(take_while(is_whitespace), cut(tag_end("}}")))),
Expand Down
8 changes: 2 additions & 6 deletions oxiplate-derive/tests/expansion/expected/concat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,8 @@ impl ::std::fmt::Display for User {
f.write_fmt(
format_args!(
"{0}", ::alloc::__export::must_use({ let res =
::alloc::fmt::format(format_args!("{0}{1}", self.name,
::alloc::__export::must_use({ let res =
::alloc::fmt::format(format_args!("{0}{1}", " (",
::alloc::__export::must_use({ let res =
::alloc::fmt::format(format_args!("{0}{1}", self.company, ")")); res
}))); res }))); res })
::alloc::fmt::format(format_args!("{0} ({1})", self.name, self.company));
res })
),
)?;
Ok(())
Expand Down

0 comments on commit 23d7c75

Please sign in to comment.