Rename duster module to parser
This commit is contained in:
10
src/parser/mod.rs
Normal file
10
src/parser/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! This module contains a rust implementation of LinkedIn Dust
|
||||
|
||||
mod node_invoker;
|
||||
mod parser;
|
||||
|
||||
pub use node_invoker::run_node_dust;
|
||||
pub use node_invoker::NodeError;
|
||||
pub use node_invoker::Result;
|
||||
pub use parser::template;
|
||||
pub use parser::Template;
|
||||
61
src/parser/node_invoker.rs
Normal file
61
src/parser/node_invoker.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
use std::io::Write;
|
||||
use std::process::Output;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, NodeError>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NodeError {
|
||||
output: Output,
|
||||
}
|
||||
|
||||
impl fmt::Display for NodeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Error from node: {}",
|
||||
String::from_utf8_lossy(&self.output.stderr)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for NodeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Error from node: {}",
|
||||
String::from_utf8_lossy(&self.output.stderr)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for NodeError {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Invokes Node to run the authentic LinkedIn Dust
|
||||
pub fn run_node_dust(template_path: &str, context: &str) -> Result<String> {
|
||||
let mut proc = Command::new("node")
|
||||
.arg("./src/js/dustjs_shim.js")
|
||||
.arg(template_path)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed to execute process");
|
||||
proc.stdin
|
||||
.take()
|
||||
.unwrap()
|
||||
.write_all(context.as_bytes())
|
||||
.expect("Failed to write to stdin of node process");
|
||||
let output = proc.wait_with_output().expect("Failed to wait on node");
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8(output.stdout).expect("Invalid UTF-8 from node process"))
|
||||
} else {
|
||||
Err(NodeError { output: output })
|
||||
}
|
||||
}
|
||||
456
src/parser/parser.rs
Normal file
456
src/parser/parser.rs
Normal file
@@ -0,0 +1,456 @@
|
||||
use nom::branch::alt;
|
||||
use nom::bytes::complete::is_a;
|
||||
use nom::bytes::complete::tag;
|
||||
use nom::bytes::complete::take_until;
|
||||
use nom::character::complete::one_of;
|
||||
use nom::combinator::map;
|
||||
use nom::combinator::opt;
|
||||
use nom::combinator::recognize;
|
||||
use nom::combinator::rest;
|
||||
use nom::combinator::value;
|
||||
use nom::combinator::verify;
|
||||
use nom::multi::many0;
|
||||
use nom::multi::many1;
|
||||
use nom::multi::separated_list;
|
||||
use nom::sequence::delimited;
|
||||
use nom::sequence::preceded;
|
||||
use nom::sequence::tuple;
|
||||
use nom::IResult;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum DustTag<'a> {
|
||||
DTSpecial(Special),
|
||||
DTComment(Comment<'a>),
|
||||
DTReference(Reference<'a>),
|
||||
DTSection(Container<'a>),
|
||||
DTExists(Container<'a>),
|
||||
DTNotExists(Container<'a>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Special {
|
||||
Space,
|
||||
NewLine,
|
||||
CarriageReturn,
|
||||
LeftCurlyBrace,
|
||||
RightCurlyBrace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Comment<'a> {
|
||||
value: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Path<'a> {
|
||||
keys: Vec<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Reference<'a> {
|
||||
path: Path<'a>,
|
||||
filters: Vec<Filter>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Filter {
|
||||
HtmlEncode,
|
||||
DisableHtmlEncode,
|
||||
JavascriptStringEncode,
|
||||
EncodeUri,
|
||||
EncodeUriComponent,
|
||||
JsonStringify,
|
||||
JsonParse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Span<'a> {
|
||||
contents: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Container<'a> {
|
||||
path: Path<'a>,
|
||||
contents: Option<Block<'a>>,
|
||||
else_contents: Option<Block<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Block<'a> {
|
||||
elements: Vec<TemplateElement<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
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((
|
||||
value(Special::LeftCurlyBrace, tag("lb")),
|
||||
value(Special::RightCurlyBrace, tag("rb")),
|
||||
value(Special::Space, tag("s")),
|
||||
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((
|
||||
one_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$"),
|
||||
opt(is_a(
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789-",
|
||||
)),
|
||||
))),
|
||||
),
|
||||
|body| Path { keys: body },
|
||||
)(i)
|
||||
}
|
||||
|
||||
fn reference(i: &str) -> IResult<&str, Reference> {
|
||||
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| {
|
||||
let (i, (opening_name, inner, maybe_else, _closing_name)) = verify(
|
||||
tuple((
|
||||
delimited(tag(open_matcher), path, tag("}")),
|
||||
opt(block),
|
||||
opt(preceded(tag("{:else}"), opt(block))),
|
||||
delimited(tag("{/"), path, tag("}")),
|
||||
)),
|
||||
|(open, _inn, _maybe_else, close)| open == close,
|
||||
)(i)?;
|
||||
|
||||
Ok((
|
||||
i,
|
||||
constructor(Container {
|
||||
path: opening_name,
|
||||
contents: inner,
|
||||
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,
|
||||
else_contents: None,
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
let (remaining, body) = verify(alt((take_until("{"), rest)), |s: &str| s.len() > 0)(i)?;
|
||||
Ok((remaining, Span { contents: body }))
|
||||
}
|
||||
|
||||
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,
|
||||
Block {
|
||||
elements: template_elements,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn template(i: &str) -> IResult<&str, Template> {
|
||||
let (remaining, contents) = block(i)?;
|
||||
Ok((remaining, Template { contents: contents }))
|
||||
}
|
||||
|
||||
#[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)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_section_mismatched_paths() {
|
||||
assert_eq!(
|
||||
super::dust_tag("{#foo.bar}{/baz}"),
|
||||
Err(Error(("{#foo.bar}{/baz}", ErrorKind::Tag)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_section() {
|
||||
assert_eq!(
|
||||
super::dust_tag("{#foo.bar}{/foo.bar}"),
|
||||
Ok((
|
||||
"",
|
||||
DustTag::DTSection(Container {
|
||||
path: Path {
|
||||
keys: vec!["foo", "bar"]
|
||||
},
|
||||
contents: None,
|
||||
else_contents: None,
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_self_closing_section() {
|
||||
assert_eq!(
|
||||
super::dust_tag("{#foo.bar/}"),
|
||||
Ok((
|
||||
"",
|
||||
DustTag::DTSection(Container {
|
||||
path: Path {
|
||||
keys: vec!["foo", "bar"]
|
||||
},
|
||||
contents: None,
|
||||
else_contents: None,
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_section_with_body() {
|
||||
assert_eq!(
|
||||
super::dust_tag("{#foo.bar}hello {name}{/foo.bar}"),
|
||||
Ok((
|
||||
"",
|
||||
DustTag::DTSection(Container {
|
||||
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()
|
||||
}))
|
||||
]
|
||||
}),
|
||||
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()
|
||||
}))
|
||||
]
|
||||
}),
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user