Skip to content

Commit

Permalink
feat: concat operator (~)
Browse files Browse the repository at this point in the history
  • Loading branch information
0b10011 committed Jan 9, 2025
1 parent 31b123c commit fb15b38
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 51 deletions.
44 changes: 40 additions & 4 deletions oxiplate-derive/src/syntax/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ 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>>,
),
Calc(
Box<ExpressionAccess<'a>>,
Operator<'a>,
Expand Down Expand Up @@ -191,6 +196,10 @@ impl ToTokens for Expression<'_> {
}
}
},
Expression::Concat(left, source, right) => {
let span = source.span();
quote_spanned! {span=> format!("{}{}", #left, #right) }
}
Expression::Calc(left, operator, right) => quote!(#left #operator #right),
Expression::Prefixed(operator, expression) => quote!(#operator #expression),
Expression::String(string) => {
Expand Down Expand Up @@ -331,15 +340,16 @@ impl ToTokens for PrefixOperator<'_> {

pub(super) fn expression<'a>(
state: &'a State,
allow_calc: bool,
allow_recursion: bool,
) -> impl Fn(Source) -> Res<Source, ExpressionAccess> + 'a {
move |input| {
let (input, (expression, fields)) = pair(
alt((
concat(state, allow_recursion),
calc(state, allow_recursion),
string,
number,
bool,
calc(state, allow_calc),
identifier(state),
prefixed_expression(state),
)),
Expand Down Expand Up @@ -473,9 +483,35 @@ fn string(input: Source) -> Res<Source, Expression> {
};
Ok((input, Expression::String(full_string)))
}
fn calc<'a>(state: &'a State, allow_calc: bool) -> impl Fn(Source) -> Res<Source, Expression> + 'a {
fn concat<'a>(
state: &'a State,
allow_recursion: bool,
) -> impl Fn(Source) -> Res<Source, Expression> + 'a {
move |input| {
if !allow_recursion {
return fail(input);
}
let (input, (left, _leading_whitespace, tilde, _trailing_whitespace, right)) =
tuple((
expression(state, false),
opt(whitespace),
tag("~"),
opt(whitespace),
context("Expected an expression", cut(expression(state, true))),
))(input)?;
Ok((
input,
Expression::Concat(Box::new(left), tilde, Box::new(right)),
))
}
}

fn calc<'a>(
state: &'a State,
allow_recursion: bool,
) -> impl Fn(Source) -> Res<Source, Expression> + 'a {
move |input| {
if !allow_calc {
if !allow_recursion {
return fail(input);
}
let (input, (left, _leading_whitespace, (), operator, _trailing_whitespace, right)) =
Expand Down
65 changes: 38 additions & 27 deletions oxiplate-derive/src/syntax/statement/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,10 @@ pub(super) fn statement<'a>(
)(input)?;

// Parse the closing tag and any trailing whitespace
let (mut input, mut trailing_whitespace) =
preceded(take_while(is_whitespace), cut(tag_end("%}")))(input)?;
let (mut input, mut trailing_whitespace) = preceded(
take_while(is_whitespace),
context(r#""%}" expected"#, cut(tag_end("%}"))),
)(input)?;

if !statement.is_ended(input.as_str().is_empty()) {
// Append trailing whitespace
Expand All @@ -158,12 +160,45 @@ pub(super) fn statement<'a>(
for value in state.local_variables {
local_variables.insert(value);
}

{
let is_eof = input.as_str().is_empty();
if is_eof {
macro_rules! context_message {
($lit:literal) => {
concat!(
r#"""#,
$lit,
r#"" statement is never closed (unexpected end of template)"#
)
};
}
let context_message = match statement.kind {
StatementKind::Block(_) => context_message!("block"),
StatementKind::If(_) => context_message!("if"),
StatementKind::For(_) => context_message!("for"),
StatementKind::Extends(_)
| StatementKind::EndBlock
| StatementKind::ElseIf(_)
| StatementKind::Else
| StatementKind::EndIf
| StatementKind::EndFor => unreachable!(
"These blocks should never fail to be closed because of EOF"
),
};
return context(context_message, fail)(input);
}
}

let state = State {
local_variables: &local_variables,
config: state.config,
};

let (new_input, items) = parse_item(&state, &should_output_blocks)(input)?;
let (new_input, items) = context(
"Failed to parse contents of statement",
cut(parse_item(&state, &should_output_blocks)),
)(input)?;
input = new_input;
for item in items {
if statement.is_ended(false) {
Expand All @@ -179,30 +214,6 @@ pub(super) fn statement<'a>(
let is_eof = input.as_str().is_empty();
if statement.is_ended(is_eof) {
break;
} else if is_eof {
macro_rules! context_message {
($lit:literal) => {
concat!(
r#"""#,
$lit,
r#"" statement is never closed (unexpected end of template)"#
)
};
}
let context_message = match statement.kind {
StatementKind::Block(_) => context_message!("block"),
StatementKind::If(_) => context_message!("if"),
StatementKind::For(_) => context_message!("for"),
StatementKind::Extends(_)
| StatementKind::EndBlock
| StatementKind::ElseIf(_)
| StatementKind::Else
| StatementKind::EndIf
| StatementKind::EndFor => unreachable!(
"These blocks should never fail to be closed because of EOF"
),
};
return context(context_message, fail)(input);
}
}
}
Expand Down
8 changes: 3 additions & 5 deletions oxiplate-derive/tests/broken/raw-rust-string.stderr
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
error: Backtrace:
[line 4, column 36] Expected '-', found '== 3 %}'
[line 4, column 36] Alt in "== 3 %}"
--> tests/broken/raw-rust-string.rs:4:37
error: "if" statement is never closed (unexpected end of template)
--> tests/broken/raw-rust-string.rs:4:44
|
4 | #[oxiplate_inline = r##"{% if "foo" == 3 %}"##]
| ^
| ^
4 changes: 3 additions & 1 deletion oxiplate-derive/tests/calc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use oxiplate_derive::Oxiplate;

#[derive(Oxiplate)]
#[oxiplate_inline = "{-}
1 + 2 = {{ 1 + 2 }}
{{ max }} + {{ min }} = {{ max + min }}
{{ max }} - {{ min }} = {{ max - min }}
{{ max }} * {{ min }} = {{ max * min }}
Expand All @@ -21,7 +22,8 @@ fn test_math() {

assert_eq!(
format!("{data}"),
"89 + 19 = 108
"1 + 2 = 3
89 + 19 = 108
89 - 19 = 70
89 * 19 = 1691
89 / 19 = 4
Expand Down
18 changes: 18 additions & 0 deletions oxiplate-derive/tests/concat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use oxiplate_derive::Oxiplate;

#[derive(Oxiplate)]
#[oxiplate_inline = r#"{{ name ~ " (" ~ company ~ ")" }}"#]
struct User {
name: &'static str,
company: &'static str,
}

#[test]
fn variable() {
let data = User {
name: "Xavier",
company: "XYZ Company",
};

assert_eq!(format!("{data}"), "Xavier (XYZ Company)");
}
30 changes: 16 additions & 14 deletions oxiplate-derive/tests/expansion/expected/calc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::prelude::rust_2021::*;
extern crate std;
use oxiplate_derive::Oxiplate;
#[oxiplate_inline = "{-}
1 + 2 = {{ 1 + 2 }}
{{ max }} + {{ min }} = {{ max + min }}
{{ max }} - {{ min }} = {{ max - min }}
{{ max }} * {{ min }} = {{ max * min }}
Expand All @@ -21,13 +22,13 @@ impl ::std::fmt::Display for Math {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
f.write_fmt(
format_args!(
"{0} + {1} = {2}\n{3} - {4} = {5}\n{6} * {7} = {8}\n{9} / {10} = {11}\n{12} % {13} = {14}\n{15} + {16} * {17} = {18}\n{19} + {20} / {21} = {22}\n{23} - {24} % {25} = {26}",
self.max, self.min, self.max + self.min, self.max, self.min, self.max -
self.min, self.max, self.min, self.max * self.min, self.max, self.min,
self.max / self.min, self.max, self.min, self.max % self.min, self.min,
self.min, self.max, self.min + self.min * self.max, self.max, self.max,
self.min, self.max + self.max / self.min, self.max, self.min, self.min,
self.max - self.min % self.min
"1 + 2 = {0}\n{1} + {2} = {3}\n{4} - {5} = {6}\n{7} * {8} = {9}\n{10} / {11} = {12}\n{13} % {14} = {15}\n{16} + {17} * {18} = {19}\n{20} + {21} / {22} = {23}\n{24} - {25} % {26} = {27}",
1 + 2, self.max, self.min, self.max + self.min, self.max, self.min, self
.max - self.min, self.max, self.min, self.max * self.min, self.max, self
.min, self.max / self.min, self.max, self.min, self.max % self.min, self
.min, self.min, self.max, self.min + self.min * self.max, self.max, self
.max, self.min, self.max + self.max / self.min, self.max, self.min, self
.min, self.max - self.min % self.min
),
)?;
Ok(())
Expand All @@ -43,9 +44,9 @@ pub const test_math: test::TestDescAndFn = test::TestDescAndFn {
ignore: false,
ignore_message: ::core::option::Option::None,
source_file: "oxiplate-derive\\tests\\calc.rs",
start_line: 19usize,
start_line: 20usize,
start_col: 4usize,
end_line: 19usize,
end_line: 20usize,
end_col: 13usize,
compile_fail: false,
no_run: false,
Expand All @@ -61,7 +62,8 @@ fn test_math() {
let res = ::alloc::fmt::format(format_args!("{0}", data));
res
}),
&"89 + 19 = 108
&"1 + 2 = 3
89 + 19 = 108
89 - 19 = 70
89 * 19 = 1691
89 / 19 = 4
Expand Down Expand Up @@ -118,9 +120,9 @@ pub const test_comparisons: test::TestDescAndFn = test::TestDescAndFn {
ignore: false,
ignore_message: ::core::option::Option::None,
source_file: "oxiplate-derive\\tests\\calc.rs",
start_line: 49usize,
start_line: 51usize,
start_col: 4usize,
end_line: 49usize,
end_line: 51usize,
end_col: 20usize,
compile_fail: false,
no_run: false,
Expand Down Expand Up @@ -206,9 +208,9 @@ pub const test_or_and: test::TestDescAndFn = test::TestDescAndFn {
ignore: false,
ignore_message: ::core::option::Option::None,
source_file: "oxiplate-derive\\tests\\calc.rs",
start_line: 85usize,
start_line: 87usize,
start_col: 4usize,
end_line: 85usize,
end_line: 87usize,
end_col: 15usize,
compile_fail: false,
no_run: false,
Expand Down
80 changes: 80 additions & 0 deletions oxiplate-derive/tests/expansion/expected/concat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use oxiplate_derive::Oxiplate;
#[oxiplate_inline = r#"{{ name ~ " (" ~ company ~ ")" }}"#]
struct User {
name: &'static str,
company: &'static str,
}
impl ::std::fmt::Display for User {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
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 })
),
)?;
Ok(())
}
}
extern crate test;
#[cfg(test)]
#[rustc_test_marker = "variable"]
#[doc(hidden)]
pub const variable: test::TestDescAndFn = test::TestDescAndFn {
desc: test::TestDesc {
name: test::StaticTestName("variable"),
ignore: false,
ignore_message: ::core::option::Option::None,
source_file: "oxiplate-derive\\tests\\concat.rs",
start_line: 11usize,
start_col: 4usize,
end_line: 11usize,
end_col: 12usize,
compile_fail: false,
no_run: false,
should_panic: test::ShouldPanic::No,
test_type: test::TestType::IntegrationTest,
},
testfn: test::StaticTestFn(#[coverage(off)] || test::assert_test_result(variable())),
};
fn variable() {
let data = User {
name: "Xavier",
company: "XYZ Company",
};
match (
&::alloc::__export::must_use({
let res = ::alloc::fmt::format(format_args!("{0}", data));
res
}),
&"Xavier (XYZ Company)",
) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
}
#[rustc_main]
#[coverage(off)]
#[doc(hidden)]
pub fn main() -> () {
extern crate test;
test::test_main_static(&[&variable])
}

0 comments on commit fb15b38

Please sign in to comment.