duster/src/parser/parser.rs

457 lines
12 KiB
Rust
Raw Normal View History

use nom::branch::alt;
use nom::bytes::complete::is_a;
use nom::bytes::complete::tag;
use nom::bytes::complete::take_until;
2020-04-05 02:45:56 +00:00
use nom::character::complete::one_of;
use nom::combinator::map;
2020-04-05 02:45:56 +00:00
use nom::combinator::opt;
use nom::combinator::recognize;
use nom::combinator::rest;
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;
use nom::multi::separated_list;
use nom::sequence::delimited;
2020-04-05 00:51:10 +00:00
use nom::sequence::preceded;
use nom::sequence::tuple;
use nom::IResult;
2020-04-05 03:42:27 +00:00
#[derive(Clone, Debug, PartialEq)]
enum DustTag<'a> {
DTSpecial(Special),
DTComment(Comment<'a>),
DTReference(Reference<'a>),
DTSection(Container<'a>),
DTExists(Container<'a>),
DTNotExists(Container<'a>),
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
enum Special {
Space,
NewLine,
CarriageReturn,
LeftCurlyBrace,
RightCurlyBrace,
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
struct Comment<'a> {
value: &'a str,
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
struct Path<'a> {
keys: Vec<&'a str>,
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
struct Reference<'a> {
path: Path<'a>,
2020-04-05 00:51:10 +00:00
filters: Vec<Filter>,
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
2020-04-05 00:51:10 +00:00
enum Filter {
HtmlEncode,
DisableHtmlEncode,
JavascriptStringEncode,
EncodeUri,
EncodeUriComponent,
JsonStringify,
JsonParse,
}
2020-04-05 02:45:56 +00:00
#[derive(Clone, Debug, PartialEq)]
struct Span<'a> {
contents: &'a str,
}
2020-04-05 03:42:27 +00:00
#[derive(Clone, Debug, PartialEq)]
struct Container<'a> {
2020-04-05 03:42:27 +00:00
path: Path<'a>,
contents: Option<Block<'a>>,
2020-04-05 21:24:13 +00:00
else_contents: Option<Block<'a>>,
2020-04-05 03:42:27 +00:00
}
#[derive(Clone, Debug, PartialEq)]
struct Block<'a> {
elements: Vec<TemplateElement<'a>>,
}
2020-04-05 02:52:14 +00:00
#[derive(Clone, Debug)]
2020-04-05 03:42:27 +00:00
pub struct Template<'a> {
contents: Block<'a>,
}
#[derive(Clone, Debug, PartialEq)]
enum TemplateElement<'a> {
TESpan(Span<'a>),
TETag(DustTag<'a>),
}
fn dust_tag(i: &str) -> IResult<&str, DustTag> {
alt((
map(special, DustTag::DTSpecial),
map(comment, DustTag::DTComment),
map(reference, DustTag::DTReference),
container("{#", DustTag::DTSection),
container("{?", DustTag::DTExists),
container("{^", DustTag::DTNotExists),
))(i)
}
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)
}
fn comment(i: &str) -> IResult<&str, Comment> {
map(delimited(tag("{!"), take_until("!}"), tag("!}")), |body| {
Comment { value: body }
})(i)
}
fn path(i: &str) -> IResult<&str, Path> {
map(
separated_list(
tag("."),
recognize(tuple((
2020-04-05 02:45:56 +00:00
one_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$"),
opt(is_a(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789-",
)),
))),
),
|body| Path { keys: body },
)(i)
}
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 container<'a, F>(
open_matcher: &'static str,
constructor: F,
) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>>
where
F: Copy + Fn(Container<'a>) -> DustTag<'a>,
{
alt((
container_with_body(open_matcher, constructor),
self_closing_container(open_matcher, constructor),
))
}
fn container_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(block),
2020-04-05 21:24:13 +00:00
opt(preceded(tag("{:else}"), opt(block))),
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_container<'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-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)
}
/// Any text that is not a Dust element
fn span(i: &str) -> IResult<&str, Span> {
2020-04-05 02:45:56 +00:00
let (remaining, body) = verify(alt((take_until("{"), rest)), |s: &str| s.len() > 0)(i)?;
Ok((remaining, Span { contents: body }))
}
2020-04-05 03:42:27 +00:00
fn block(i: &str) -> IResult<&str, Block> {
let (remaining, template_elements) = many1(alt((
map(span, TemplateElement::TESpan),
map(dust_tag, TemplateElement::TETag),
)))(i)?;
Ok((
remaining,
2020-04-05 03:42:27 +00:00
Block {
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> {
let (remaining, contents) = block(i)?;
Ok((remaining, Template { contents: contents }))
}
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() {
assert_eq!(
super::span("this is just some text"),
Ok((
"",
Span {
contents: "this is just some text"
}
))
);
assert_eq!(
super::span("this is just some text {~lb}"),
Ok((
"{~lb}",
Span {
contents: "this is just some text "
}
))
);
assert_eq!(
super::span("{~lb}"),
Err(Error(("{~lb}", ErrorKind::Verify)))
);
}
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(Block {
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
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(Block {
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
}))
]
}),
else_contents: Some(Block {
elements: vec![
TemplateElement::TESpan(Span {
contents: "goodbye "
}),
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
}))
]
}),
})
2020-04-05 03:42:27 +00:00
))
);
}
2020-04-05 02:45:56 +00:00
}