duster/src/parser/parser.rs

1334 lines
41 KiB
Rust
Raw Normal View History

use nom::branch::alt;
use nom::bytes::complete::escaped_transform;
use nom::bytes::complete::is_a;
use nom::bytes::complete::is_not;
2020-05-11 03:01:14 +00:00
use nom::bytes::complete::{tag, take_until, take_until_parser_matches, take_while};
2020-04-12 20:02:26 +00:00
use nom::character::complete::line_ending;
use nom::character::complete::multispace0;
2020-04-05 02:45:56 +00:00
use nom::character::complete::one_of;
2020-05-11 03:01:14 +00:00
use nom::character::complete::{digit1, space0, space1};
2020-04-12 20:02:26 +00:00
use nom::combinator::all_consuming;
use nom::combinator::map;
2020-04-05 02:45:56 +00:00
use nom::combinator::opt;
use nom::combinator::recognize;
use nom::combinator::value;
2020-04-05 02:45:56 +00:00
use nom::combinator::verify;
2020-04-05 00:51:10 +00:00
use nom::multi::many0;
2020-04-05 03:42:27 +00:00
use nom::multi::many1;
2020-04-12 21:39:24 +00:00
use nom::multi::separated_list1;
use nom::sequence::delimited;
2020-04-05 00:51:10 +00:00
use nom::sequence::preceded;
use nom::sequence::separated_pair;
2020-04-08 01:49:28 +00:00
use nom::sequence::terminated;
use nom::sequence::tuple;
use nom::IResult;
2020-04-05 03:42:27 +00:00
#[derive(Clone, Debug, PartialEq)]
2020-04-11 22:25:48 +00:00
pub enum DustTag<'a> {
DTSpecial(Special),
DTComment(Comment<'a>),
2020-05-24 03:41:05 +00:00
DTLiteralStringBlock(&'a str),
DTReference(Reference<'a>),
DTSection(Container<'a>),
DTExists(Container<'a>),
DTNotExists(Container<'a>),
2020-04-06 03:47:55 +00:00
DTBlock(NamedBlock<'a>),
DTInlinePartial(NamedBlock<'a>),
DTPartial(Partial<'a>),
2020-04-08 01:49:28 +00:00
DTHelperEquals(ParameterizedBlock<'a>),
DTHelperNotEquals(ParameterizedBlock<'a>),
DTHelperGreaterThan(ParameterizedBlock<'a>),
DTHelperLessThan(ParameterizedBlock<'a>),
2020-05-16 23:05:03 +00:00
DTHelperGreaterThanOrEquals(ParameterizedBlock<'a>),
DTHelperLessThanOrEquals(ParameterizedBlock<'a>),
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
pub enum Special {
Space,
NewLine,
CarriageReturn,
LeftCurlyBrace,
RightCurlyBrace,
}
#[derive(Clone, Debug, PartialEq)]
pub enum IgnoredWhitespace<'a> {
StartOfLine(&'a str),
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
pub struct Comment<'a> {
value: &'a str,
}
/// A series of keys separated by '.' to reference a variable in the context
///
/// Special case: If the path is just "." then keys will be an empty vec
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
pub struct Path<'a> {
pub keys: Vec<&'a str>,
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
pub struct Reference<'a> {
pub path: Path<'a>,
pub filters: Vec<Filter>,
2020-04-05 00:51:10 +00:00
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
pub enum Filter {
2020-04-05 00:51:10 +00:00
HtmlEncode,
DisableHtmlEncode,
JavascriptStringEncode,
EncodeUri,
EncodeUriComponent,
JsonStringify,
JsonParse,
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
2020-04-11 22:25:48 +00:00
pub struct Span<'a> {
pub contents: &'a str,
}
2020-04-05 03:42:27 +00:00
#[derive(Clone, Debug, PartialEq)]
pub struct Container<'a> {
pub path: Path<'a>,
pub contents: Option<Body<'a>>,
pub else_contents: Option<Body<'a>>,
2020-04-05 03:42:27 +00:00
}
2020-04-06 03:47:55 +00:00
#[derive(Clone, Debug, PartialEq)]
pub struct NamedBlock<'a> {
pub name: &'a str,
pub contents: Option<Body<'a>>,
2020-04-06 03:47:55 +00:00
}
2020-04-07 02:02:10 +00:00
#[derive(Clone, Debug, PartialEq)]
pub struct ParameterizedBlock<'a> {
2020-05-10 18:22:59 +00:00
pub name: &'a str,
pub params: Vec<KVPair<'a>>,
pub contents: Option<Body<'a>>,
pub else_contents: Option<Body<'a>>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Partial<'a> {
pub name: Vec<PartialNameElement>,
pub params: Vec<KVPair<'a>>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum OwnedLiteral {
LString(String),
LPositiveInteger(u64),
}
#[derive(Clone, Debug, PartialEq)]
pub enum RValue<'a> {
RVPath(Path<'a>),
RVLiteral(OwnedLiteral),
}
#[derive(Clone, Debug, PartialEq)]
pub struct KVPair<'a> {
pub key: &'a str,
pub value: RValue<'a>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PartialNameElement {
PNSpan {
contents: String,
},
PNReference {
path: Vec<String>,
filters: Vec<Filter>,
},
}
2020-04-05 03:42:27 +00:00
#[derive(Clone, Debug, PartialEq)]
2020-04-11 22:25:48 +00:00
pub struct Body<'a> {
pub elements: Vec<TemplateElement<'a>>,
}
#[derive(Clone, Debug, PartialEq)]
2020-04-05 03:42:27 +00:00
pub struct Template<'a> {
2020-04-11 22:25:48 +00:00
pub contents: Body<'a>,
2020-04-05 03:42:27 +00:00
}
#[derive(Clone, Debug, PartialEq)]
2020-04-11 22:25:48 +00:00
pub enum TemplateElement<'a> {
TESpan(Span<'a>),
TETag(DustTag<'a>),
TEIgnoredWhitespace(IgnoredWhitespace<'a>),
}
impl From<TemplateElement<'_>> for PartialNameElement {
fn from(original: TemplateElement) -> Self {
match original {
TemplateElement::TESpan(span) => PartialNameElement::PNSpan {
contents: span.contents.to_owned(),
},
TemplateElement::TETag(DustTag::DTReference(reference)) => {
PartialNameElement::PNReference {
path: reference
.path
.keys
.into_iter()
.map(|s| s.to_owned())
.collect(),
filters: reference.filters,
}
}
_ => panic!("Only spans and references can be used in partial names."),
}
}
}
impl<'a> From<&'a PartialNameElement> for TemplateElement<'a> {
fn from(original: &'a PartialNameElement) -> Self {
match original {
PartialNameElement::PNSpan { contents } => {
TemplateElement::TESpan(Span { contents: contents })
}
PartialNameElement::PNReference { path, filters } => {
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path {
keys: path.into_iter().map(|s| s.as_str()).collect(),
},
filters: filters.into_iter().map(|f| f.clone()).collect(),
}))
}
}
}
}
/// Any element significant to dust that isn't plain text
///
/// These elements are always wrapped in curly braces
fn dust_tag(i: &str) -> IResult<&str, DustTag> {
alt((
map(special, DustTag::DTSpecial),
map(comment, DustTag::DTComment),
2020-05-24 03:41:05 +00:00
map(literal_string_block, DustTag::DTLiteralStringBlock),
map(reference, DustTag::DTReference),
conditional("{#", DustTag::DTSection),
conditional("{?", DustTag::DTExists),
conditional("{^", DustTag::DTNotExists),
named_block("{+", DustTag::DTBlock),
named_block("{<", DustTag::DTInlinePartial),
partial("{>", DustTag::DTPartial),
2020-05-16 23:05:03 +00:00
parameterized_block("{@", "gte", DustTag::DTHelperGreaterThanOrEquals),
parameterized_block("{@", "lte", DustTag::DTHelperLessThanOrEquals),
parameterized_block("{@", "eq", DustTag::DTHelperEquals),
parameterized_block("{@", "ne", DustTag::DTHelperNotEquals),
parameterized_block("{@", "gt", DustTag::DTHelperGreaterThan),
parameterized_block("{@", "lt", DustTag::DTHelperLessThan),
))(i)
}
/// Special characters
fn special(i: &str) -> IResult<&str, Special> {
delimited(
tag("{~"),
alt((
2020-04-05 02:45:56 +00:00
value(Special::LeftCurlyBrace, tag("lb")),
value(Special::RightCurlyBrace, tag("rb")),
value(Special::Space, tag("s")),
2020-04-05 02:45:56 +00:00
value(Special::NewLine, tag("n")),
value(Special::CarriageReturn, tag("r")),
)),
tag("}"),
)(i)
}
/// Part of a dust template that does not get rendered
fn comment(i: &str) -> IResult<&str, Comment> {
map(delimited(tag("{!"), take_until("!}"), tag("!}")), |body| {
Comment { value: body }
})(i)
}
/// A single element of a path
fn key(i: &str) -> IResult<&str, &str> {
recognize(tuple((
one_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$"),
opt(is_a(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789-",
)),
)))(i)
}
/// A series of keys separated by '.' to reference a variable in the context
fn path(i: &str) -> IResult<&str, Path> {
alt((
map(separated_list1(tag("."), key), |body| Path { keys: body }),
map(
tuple((tag("."), separated_list1(tag("."), key))),
|(dot, mut body)| {
body.insert(0, dot);
Path { keys: body }
},
),
map(tag("."), |dot| Path { keys: vec![dot] }),
))(i)
}
2020-05-11 03:13:25 +00:00
/// Just digits, no signs or decimals
fn postitive_integer_literal(i: &str) -> IResult<&str, u64> {
2020-05-11 03:01:14 +00:00
map(
verify(
map(digit1, |number_string: &str| number_string.parse::<u64>()),
|parse_result| parse_result.is_ok(),
),
2020-05-11 03:13:25 +00:00
|parsed_number| parsed_number.unwrap(),
2020-05-11 03:01:14 +00:00
)(i)
}
/// Either a literal or a path to a value
fn rvalue(i: &str) -> IResult<&str, RValue> {
alt((
map(path, RValue::RVPath),
map(quoted_string, |s| {
RValue::RVLiteral(OwnedLiteral::LString(s))
}),
map(postitive_integer_literal, |num| {
RValue::RVLiteral(OwnedLiteral::LPositiveInteger(num))
}),
))(i)
}
/// Parameters for a partial
2020-04-07 03:35:09 +00:00
fn key_value_pair(i: &str) -> IResult<&str, KVPair> {
map(separated_pair(key, tag("="), rvalue), |(k, v)| KVPair {
key: k,
value: v,
})(i)
}
/// Display a value from the context
fn reference(i: &str) -> IResult<&str, Reference> {
2020-04-05 00:51:10 +00:00
let (remaining, (p, filters)) = delimited(tag("{"), tuple((path, many0(filter))), tag("}"))(i)?;
Ok((
remaining,
Reference {
path: p,
filters: filters,
},
))
}
fn conditional<'a, F>(
open_matcher: &'static str,
constructor: F,
2020-04-12 21:39:24 +00:00
) -> impl FnMut(&'a str) -> IResult<&'a str, DustTag<'a>>
where
F: Copy + Fn(Container<'a>) -> DustTag<'a>,
{
alt((
conditional_with_body(open_matcher, constructor),
self_closing_conditional(open_matcher, constructor),
))
}
fn conditional_with_body<'a, F>(
open_matcher: &'static str,
constructor: F,
) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>>
where
F: Fn(Container<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
2020-04-05 21:24:13 +00:00
let (i, (opening_name, inner, maybe_else, _closing_name)) = verify(
tuple((
delimited(tag(open_matcher), path, tag("}")),
opt(body),
opt(preceded(tag("{:else}"), opt(body))),
delimited(tag("{/"), path, tag("}")),
)),
2020-04-05 21:24:13 +00:00
|(open, _inn, _maybe_else, close)| open == close,
)(i)?;
Ok((
i,
constructor(Container {
path: opening_name,
contents: inner,
2020-04-05 21:24:13 +00:00
else_contents: maybe_else.flatten(),
}),
))
}
}
fn self_closing_conditional<'a, F>(
open_matcher: &'static str,
constructor: F,
) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>>
where
F: Fn(Container<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, path) = delimited(tag(open_matcher), path, tag("/}"))(i)?;
Ok((
i,
constructor(Container {
path: path,
contents: None,
2020-04-05 21:24:13 +00:00
else_contents: None,
}),
))
}
}
2020-04-06 03:47:55 +00:00
fn named_block<'a, F>(
open_matcher: &'static str,
constructor: F,
2020-04-12 21:39:24 +00:00
) -> impl FnMut(&'a str) -> IResult<&'a str, DustTag<'a>>
2020-04-06 03:47:55 +00:00
where
F: Copy + Fn(NamedBlock<'a>) -> DustTag<'a>,
{
alt((
named_block_with_body(open_matcher, constructor),
self_closing_named_block(open_matcher, constructor),
))
}
fn named_block_with_body<'a, F>(
open_matcher: &'static str,
constructor: F,
) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>>
where
F: Fn(NamedBlock<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, (opening_name, inner, _closing_name)) = verify(
tuple((
delimited(tag(open_matcher), key, tag("}")),
opt(body),
delimited(tag("{/"), key, tag("}")),
)),
|(open, _inn, close)| open == close,
)(i)?;
Ok((
i,
constructor(NamedBlock {
name: opening_name,
contents: inner,
}),
))
}
}
fn self_closing_named_block<'a, F>(
open_matcher: &'static str,
constructor: F,
) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>>
where
F: Fn(NamedBlock<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, name) = delimited(tag(open_matcher), key, tag("/}"))(i)?;
Ok((
i,
constructor(NamedBlock {
name: name,
contents: None,
}),
))
}
}
2020-04-08 01:49:28 +00:00
fn parameterized_block<'a, F>(
open_matcher: &'static str,
tag_name: &'static str,
constructor: F,
2020-04-12 21:39:24 +00:00
) -> impl FnMut(&'a str) -> IResult<&'a str, DustTag<'a>>
2020-04-08 01:49:28 +00:00
where
F: Copy + Fn(ParameterizedBlock<'a>) -> DustTag<'a>,
{
alt((
parameterized_block_with_body(open_matcher, tag_name, constructor),
parameterized_self_closing_block(open_matcher, tag_name, constructor),
))
}
fn parameterized_block_with_body<'a, F>(
open_matcher: &'static str,
tag_name: &'static str,
constructor: F,
) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>>
where
F: Fn(ParameterizedBlock<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, (name, params, inner, maybe_else, _closing_name)) = tuple((
preceded(tag(open_matcher), tag(tag_name)),
terminated(
opt(delimited(
space1,
separated_list1(space1, key_value_pair),
space0,
)),
2020-04-08 01:49:28 +00:00
tag("}"),
),
opt(body),
opt(preceded(tag("{:else}"), opt(body))),
delimited(tag("{/"), tag(tag_name), tag("}")),
))(i)?;
Ok((
i,
constructor(ParameterizedBlock {
name: name,
params: params.unwrap_or(Vec::new()),
contents: inner,
else_contents: maybe_else.flatten(),
}),
))
}
}
2020-04-07 02:02:10 +00:00
fn parameterized_self_closing_block<'a, F>(
open_matcher: &'static str,
tag_name: &'static str,
2020-04-07 02:02:10 +00:00
constructor: F,
) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>>
where
F: Fn(ParameterizedBlock<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, (name, params)) = delimited(
tag(open_matcher),
tuple((
tag(tag_name),
opt(delimited(
space1,
separated_list1(space1, key_value_pair),
space0,
)),
)),
tag("/}"),
)(i)?;
2020-04-07 02:02:10 +00:00
Ok((
i,
constructor(ParameterizedBlock {
name: name,
params: params.unwrap_or(Vec::new()),
contents: None,
else_contents: None,
}),
))
}
}
fn partial_with_plain_tag<'a>(
open_matcher: &'static str,
) -> impl Fn(&'a str) -> IResult<&'a str, Partial<'a>> {
move |i: &'a str| {
let (i, (name, params)) = delimited(
tag(open_matcher),
tuple((
key,
opt(delimited(
space1,
separated_list1(space1, key_value_pair),
space0,
)),
)),
tag("/}"),
)(i)?;
Ok((
i,
Partial {
name: vec![PartialNameElement::PNSpan {
contents: name.to_owned(),
}],
params: params.unwrap_or(Vec::new()),
},
))
}
}
fn partial_quoted_tag(i: &str) -> IResult<&str, Vec<TemplateElement>> {
2020-05-18 01:21:26 +00:00
all_consuming(many1(alt((
map(span, TemplateElement::TESpan),
map(map(reference, DustTag::DTReference), TemplateElement::TETag),
2020-05-18 01:21:26 +00:00
))))(i)
}
fn partial_with_quoted_tag<'a>(
open_matcher: &'static str,
) -> impl Fn(&'a str) -> IResult<&'a str, Partial<'a>> {
move |i: &'a str| {
let (i, (name, params)) = delimited(
tag(open_matcher),
tuple((
verify(quoted_string, |s: &String| {
partial_quoted_tag(s.as_str()).is_ok()
}),
opt(delimited(
space1,
separated_list1(space1, key_value_pair),
space0,
)),
)),
tag("/}"),
)(i)?;
let (_remaining, template_name_elements) = partial_quoted_tag(name.as_str())
.expect("A successful parse was verified earlier with a call to verify()");
let partial_name_elements = template_name_elements
.into_iter()
.map(|e| e.into())
.collect();
Ok((
i,
Partial {
name: partial_name_elements,
params: params.unwrap_or(Vec::new()),
},
))
}
}
fn partial<'a, F>(
open_matcher: &'static str,
constructor: F,
) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>>
where
F: Fn(Partial<'a>) -> DustTag<'a>,
{
let plain = partial_with_plain_tag(open_matcher);
let quoted = partial_with_quoted_tag(open_matcher);
move |i: &'a str| map(alt((&plain, &quoted)), &constructor)(i)
}
2020-04-05 00:51:10 +00:00
fn filter(i: &str) -> IResult<&str, Filter> {
preceded(
tag("|"),
alt((
value(Filter::JsonStringify, tag("js")),
value(Filter::JsonParse, tag("jp")),
value(Filter::EncodeUriComponent, tag("uc")),
value(Filter::HtmlEncode, tag("h")),
value(Filter::DisableHtmlEncode, tag("s")),
value(Filter::JavascriptStringEncode, tag("j")),
value(Filter::EncodeUri, tag("u")),
)),
)(i)
}
/// Whitespace at the beginning of lines is ignored so we are matching
/// a newline character followed by as much contiguous whitespace as
/// possible, all of which will be thrown away by other parsers.
fn ignore_new_line_leading_whitespace(i: &str) -> IResult<&str, IgnoredWhitespace> {
map(
recognize(tuple((line_ending, multispace0))),
IgnoredWhitespace::StartOfLine,
)(i)
}
2020-05-24 03:41:05 +00:00
fn literal_string_block(i: &str) -> IResult<&str, &str> {
delimited(tag("{`"), take_until("`}"), tag("`}"))(i)
}
/// Any text that is not a Dust element or ignored whitespace
fn span(i: &str) -> IResult<&str, Span> {
let (remaining, line) = verify(
take_until_parser_matches(alt((
tag("{"),
line_ending,
recognize(all_consuming(eof_whitespace)),
))),
|s: &str| s.len() > 0,
)(i)?;
Ok((remaining, Span { contents: line }))
}
fn body(i: &str) -> IResult<&str, Body> {
2020-04-05 03:42:27 +00:00
let (remaining, template_elements) = many1(alt((
map(
ignore_new_line_leading_whitespace,
TemplateElement::TEIgnoredWhitespace,
),
map(span, TemplateElement::TESpan),
map(dust_tag, TemplateElement::TETag),
)))(i)?;
Ok((
remaining,
Body {
elements: template_elements,
},
))
}
2020-04-05 02:45:56 +00:00
2020-04-05 03:42:27 +00:00
pub fn template(i: &str) -> IResult<&str, Template> {
2020-04-12 20:02:26 +00:00
// DustJS ignores all preceding whitespace (tabs, newlines, spaces) but only ignores trailing newlines
let (remaining, contents) = delimited(multispace0, body, eof_whitespace)(i)?;
2020-04-05 03:42:27 +00:00
Ok((remaining, Template { contents: contents }))
}
fn quoted_string(i: &str) -> IResult<&str, String> {
2020-04-07 01:03:03 +00:00
delimited(
tag(r#"""#),
escaped_transform(is_not(r#"\""#), '\\', one_of(r#"\""#)),
2020-04-07 01:03:03 +00:00
tag(r#"""#),
)(i)
}
2020-04-12 20:02:26 +00:00
fn eof_whitespace(i: &str) -> IResult<&str, Vec<&str>> {
many0(line_ending)(i)
}
2020-04-05 02:45:56 +00:00
#[cfg(test)]
mod tests {
use super::*;
use nom::bytes::complete::is_a;
use nom::error::ErrorKind;
use nom::Err::Error;
#[test]
fn test_reference() {
assert_eq!(
super::reference("{foo.bar.baz|js|s}"),
Ok((
"",
Reference {
path: Path {
keys: vec!["foo", "bar", "baz"]
},
filters: vec![Filter::JsonStringify, Filter::DisableHtmlEncode],
}
))
);
}
#[test]
fn test_path() {
assert_eq!(
is_a::<_, _, (_, ErrorKind)>("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$")(
"foo"
),
Ok(("", "foo"))
);
assert_eq!(
super::path("foo.bar.baz"),
Ok((
"",
Path {
keys: vec!["foo", "bar", "baz"]
}
))
);
}
#[test]
fn test_special() {
assert_eq!(super::special("{~s}"), Ok(("", Special::Space)));
assert_eq!(super::special("{~n}"), Ok(("", Special::NewLine)));
assert_eq!(super::special("{~r}"), Ok(("", Special::CarriageReturn)));
assert_eq!(super::special("{~lb}"), Ok(("", Special::LeftCurlyBrace)));
assert_eq!(super::special("{~rb}"), Ok(("", Special::RightCurlyBrace)));
assert_eq!(
super::special("{~zzz}"),
Err(Error(("zzz}", ErrorKind::Tag)))
);
}
#[test]
fn test_comment() {
assert_eq!(
super::comment("{! yo dawg} this is a comment !}"),
Ok((
"",
Comment {
value: " yo dawg} this is a comment "
}
))
);
assert_eq!(
super::special("{! this is a comment without a close"),
Err(Error((
"{! this is a comment without a close",
ErrorKind::Tag
)))
);
}
#[test]
fn test_span_end_of_line() {
assert_eq!(
super::ignore_new_line_leading_whitespace("\n \t \n\nfoo"),
Ok(("foo", IgnoredWhitespace::StartOfLine("\n \t \n\n")))
);
}
#[test]
2020-05-03 16:48:02 +00:00
fn test_span() {
assert_eq!(
2020-05-03 16:48:02 +00:00
super::span("this is just some text"),
Ok((
"",
2020-05-03 16:48:02 +00:00
Span {
contents: "this is just some text"
}
))
);
assert_eq!(
2020-05-03 16:48:02 +00:00
super::span("this is just some text {~lb}"),
Ok((
"{~lb}",
2020-05-03 16:48:02 +00:00
Span {
contents: "this is just some text "
}
))
);
assert_eq!(
2020-05-03 16:48:02 +00:00
super::span("{~lb}"),
Err(Error(("{~lb}", ErrorKind::Verify)))
);
assert_eq!(
super::body("this is \t \n\n \t \n \t multiline text\n {foo}"),
Ok((
"",
Body {
elements: vec![
TemplateElement::TESpan(Span {
contents: "this is \t "
}),
TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine(
"\n\n \t \n \t "
)),
TemplateElement::TESpan(Span {
contents: "multiline text"
}),
TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine(
"\n "
)),
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["foo"] },
filters: vec![]
}))
]
}
))
);
assert_eq!(
super::body("\n leading whitespace"),
Ok((
"",
Body {
elements: vec![
TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine(
"\n "
)),
TemplateElement::TESpan(Span {
contents: "leading whitespace"
}),
]
}
))
);
}
2020-04-05 03:42:27 +00:00
#[test]
fn test_section_mismatched_paths() {
assert_eq!(
super::dust_tag("{#foo.bar}{/baz}"),
Err(Error(("{#foo.bar}{/baz}", ErrorKind::Tag)))
2020-04-05 03:42:27 +00:00
);
}
#[test]
fn test_empty_section() {
assert_eq!(
super::dust_tag("{#foo.bar}{/foo.bar}"),
2020-04-05 03:42:27 +00:00
Ok((
"",
DustTag::DTSection(Container {
path: Path {
keys: vec!["foo", "bar"]
},
2020-04-05 21:24:13 +00:00
contents: None,
else_contents: None,
})
))
);
}
#[test]
fn test_self_closing_section() {
assert_eq!(
super::dust_tag("{#foo.bar/}"),
Ok((
"",
DustTag::DTSection(Container {
2020-04-05 03:42:27 +00:00
path: Path {
keys: vec!["foo", "bar"]
},
2020-04-05 21:24:13 +00:00
contents: None,
else_contents: None,
})
2020-04-05 03:42:27 +00:00
))
);
}
#[test]
fn test_section_with_body() {
assert_eq!(
super::dust_tag("{#foo.bar}hello {name}{/foo.bar}"),
2020-04-05 03:42:27 +00:00
Ok((
"",
DustTag::DTSection(Container {
2020-04-05 03:42:27 +00:00
path: Path {
keys: vec!["foo", "bar"]
},
contents: Some(Body {
2020-04-05 03:42:27 +00:00
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
2020-04-05 03:42:27 +00:00
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
}))
]
2020-04-05 21:24:13 +00:00
}),
else_contents: None,
})
))
);
}
#[test]
fn test_section_with_else_body() {
assert_eq!(
super::dust_tag("{#greeting}hello {name}{:else}goodbye {name}{/greeting}"),
Ok((
"",
DustTag::DTSection(Container {
path: Path {
keys: vec!["greeting"]
},
contents: Some(Body {
2020-04-05 21:24:13 +00:00
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
2020-04-05 21:24:13 +00:00
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
}))
]
}),
else_contents: Some(Body {
2020-04-05 21:24:13 +00:00
elements: vec![
TemplateElement::TESpan(Span {
contents: "goodbye "
2020-04-05 21:24:13 +00:00
}),
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
}))
]
}),
})
2020-04-05 03:42:27 +00:00
))
);
}
2020-04-06 03:47:55 +00:00
#[test]
fn test_self_closing_block() {
assert_eq!(
super::dust_tag("{+foo/}"),
Ok((
"",
DustTag::DTBlock(NamedBlock {
name: "foo",
contents: None
})
))
);
}
#[test]
fn test_block() {
assert_eq!(
super::dust_tag("{+foo}hello {name}{/foo}"),
Ok((
"",
DustTag::DTBlock(NamedBlock {
name: "foo",
contents: Some(Body {
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
2020-04-06 03:47:55 +00:00
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
}))
]
})
})
))
);
}
#[test]
fn test_self_closing_inline_partial() {
assert_eq!(
super::dust_tag("{<foo/}"),
Ok((
"",
DustTag::DTInlinePartial(NamedBlock {
name: "foo",
contents: None
})
))
);
}
#[test]
fn test_inline_partial() {
assert_eq!(
super::dust_tag("{<foo}hello {name}{/foo}"),
Ok((
"",
DustTag::DTInlinePartial(NamedBlock {
name: "foo",
contents: Some(Body {
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
2020-04-06 03:47:55 +00:00
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
}))
]
})
})
))
);
}
#[test]
fn test_quoted_string() {
assert_eq!(
quoted_string(r#""foo\"bar\\baz""#),
Ok(("", r#"foo"bar\baz"#.to_owned()))
);
}
2020-04-07 02:02:10 +00:00
#[test]
fn test_unquoted_partial() {
assert_eq!(
dust_tag(r#"{>foo bar=baz animal="cat"/}"#),
Ok((
"",
DustTag::DTPartial(Partial {
name: vec![PartialNameElement::PNSpan {
contents: "foo".to_owned()
},],
params: vec![
KVPair {
key: "bar",
value: RValue::RVPath(Path { keys: vec!["baz"] })
},
KVPair {
key: "animal",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
}
]
})
))
);
2020-04-07 02:02:10 +00:00
}
2020-04-07 23:53:57 +00:00
#[test]
fn test_quoted_partial() {
assert_eq!(
dust_tag(r#"{>"template name * with * special \" characters" bar=baz animal="cat"/}"#),
Ok((
"",
DustTag::DTPartial(Partial {
name: vec![PartialNameElement::PNSpan {
contents: r#"template name * with * special " characters"#.to_owned()
},],
params: vec![
KVPair {
key: "bar",
value: RValue::RVPath(Path { keys: vec!["baz"] })
},
KVPair {
key: "animal",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
}
]
})
))
);
}
#[test]
fn test_dynamic_partial() {
assert_eq!(
dust_tag(r#"{>"dynamic{ref}template" bar=baz animal="cat"/}"#),
Ok((
"",
DustTag::DTPartial(Partial {
name: vec![
PartialNameElement::PNSpan {
contents: "dynamic".to_owned()
},
PartialNameElement::PNReference {
path: vec!["ref".to_owned()],
filters: Vec::new()
},
PartialNameElement::PNSpan {
contents: "template".to_owned()
}
],
2020-04-07 23:53:57 +00:00
params: vec![
KVPair {
key: "bar",
value: RValue::RVPath(Path { keys: vec!["baz"] })
},
KVPair {
key: "animal",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
2020-04-07 23:53:57 +00:00
}
]
})
))
);
}
2020-04-08 01:49:28 +00:00
2020-05-11 03:42:56 +00:00
#[test]
fn test_literals() {
assert_eq!(
dust_tag(r#"{>foo a="foo" b=179/}"#),
Ok((
"",
DustTag::DTPartial(Partial {
name: vec![PartialNameElement::PNSpan {
contents: "foo".to_owned()
},],
2020-05-11 03:42:56 +00:00
params: vec![
KVPair {
key: "a",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString("foo".to_owned()))
2020-05-11 03:42:56 +00:00
},
KVPair {
key: "b",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LPositiveInteger(179))
2020-05-11 03:42:56 +00:00
}
]
})
))
);
}
2020-04-08 01:49:28 +00:00
#[test]
fn test_helper() {
assert_eq!(
dust_tag(r#"{@eq key=name value="cat"}Pet the {name}!{/eq}"#),
Ok((
"",
DustTag::DTHelperEquals(ParameterizedBlock {
name: "eq",
params: vec![
KVPair {
key: "key",
value: RValue::RVPath(Path { keys: vec!["name"] })
},
KVPair {
key: "value",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
2020-04-08 01:49:28 +00:00
}
],
contents: Some(Body {
elements: vec![
TemplateElement::TESpan(Span {
contents: "Pet the "
2020-04-08 01:49:28 +00:00
}),
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
})),
TemplateElement::TESpan(Span { contents: "!" })
2020-04-08 01:49:28 +00:00
]
}),
else_contents: None
})
))
);
}
#[test]
fn test_self_closing_helper() {
assert_eq!(
dust_tag(r#"{@eq key=name value="cat"/}"#),
Ok((
"",
DustTag::DTHelperEquals(ParameterizedBlock {
name: "eq",
params: vec![
KVPair {
key: "key",
value: RValue::RVPath(Path { keys: vec!["name"] })
},
KVPair {
key: "value",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
2020-04-08 01:49:28 +00:00
}
],
contents: None,
else_contents: None
})
))
);
}
#[test]
fn test_full_document_new_line_equality() {
assert_eq!(
super::template(
"- simple -{~n}
{#names}{.}{/names}
{~n}- new lines -{~n}
{#names}
{.}
{/names}"
),
Ok::<_, nom::Err<(&str, ErrorKind)>>((
"",
Template {
contents: Body {
elements: vec![
TemplateElement::TESpan(Span {
contents: "- simple -"
}),
TemplateElement::TETag(DustTag::DTSpecial(Special::NewLine)),
TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine(
"\n"
)),
TemplateElement::TETag(DustTag::DTSection(Container {
path: Path {
keys: vec!["names"]
},
contents: Some(Body {
elements: vec![TemplateElement::TETag(DustTag::DTReference(
Reference {
path: Path { keys: vec!["."] },
filters: vec![]
}
))]
}),
else_contents: None,
})),
TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine(
"\n"
)),
TemplateElement::TETag(DustTag::DTSpecial(Special::NewLine)),
TemplateElement::TESpan(Span {
contents: "- new lines -"
}),
TemplateElement::TETag(DustTag::DTSpecial(Special::NewLine)),
TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine(
"\n"
)),
TemplateElement::TETag(DustTag::DTSection(Container {
path: Path {
keys: vec!["names"]
},
contents: Some(Body {
elements: vec![
TemplateElement::TEIgnoredWhitespace(
IgnoredWhitespace::StartOfLine("\n")
),
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["."] },
filters: vec![]
})),
TemplateElement::TEIgnoredWhitespace(
IgnoredWhitespace::StartOfLine("\n")
)
]
}),
else_contents: None,
})),
]
}
}
))
);
}
#[test]
2020-05-09 19:11:04 +00:00
fn test_full_document_parameterized_partial() {
assert_eq!(
super::template(
r#"{#level3.level4}{>partialtwo v1="b" v2="b" v3="b" v4="b" v5="b" /}{/level3.level4}"#
),
Ok::<_, nom::Err<(&str, ErrorKind)>>((
"",
Template {
2020-05-09 19:05:29 +00:00
contents: Body {
elements: vec![TemplateElement::TETag(DustTag::DTSection(Container {
path: Path {
keys: vec!["level3", "level4"]
},
contents: Some(Body {
elements: vec![TemplateElement::TETag(DustTag::DTPartial(
Partial {
name: vec![PartialNameElement::PNSpan {
contents: "partialtwo".to_owned()
},],
params: vec![
KVPair {
key: "v1",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString(
"b".to_owned()
))
},
KVPair {
key: "v2",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString(
"b".to_owned()
))
},
KVPair {
key: "v3",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString(
"b".to_owned()
))
},
KVPair {
key: "v4",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString(
"b".to_owned()
))
},
KVPair {
key: "v5",
2020-05-17 02:39:29 +00:00
value: RValue::RVLiteral(OwnedLiteral::LString(
"b".to_owned()
))
}
]
}
))]
}),
2020-05-09 19:05:29 +00:00
else_contents: None
}))]
}
}
))
);
}
2020-04-05 02:45:56 +00:00
}