Content: Fix closing tag lookahead, support [br]

This commit is contained in:
asonix 2021-02-01 18:03:40 -06:00
parent 84d5aa39cf
commit f2552c794d
3 changed files with 101 additions and 24 deletions

View file

@ -32,6 +32,7 @@ pub enum Tag {
IconText, IconText,
Icon, Icon,
Hr, Hr,
Br,
Url, Url,
} }
@ -68,7 +69,7 @@ struct ClosingTagBackout;
impl Tag { impl Tag {
fn needs_closing(&self) -> bool { fn needs_closing(&self) -> bool {
!matches!(self, Tag::Hr) !matches!(self, Tag::Hr) && !matches!(self, Tag::Br)
} }
fn with_attribute(&self, attribute: &Option<String>) -> Option<String> { fn with_attribute(&self, attribute: &Option<String>) -> Option<String> {
@ -121,6 +122,7 @@ where
"icontext" => Tag::IconText, "icontext" => Tag::IconText,
"icon" => Tag::Icon, "icon" => Tag::Icon,
"hr" => Tag::Hr, "hr" => Tag::Hr,
"br" => Tag::Br,
"url" => Tag::Url, "url" => Tag::Url,
_ => { _ => {
return Err(StreamErrorFor::<Input>::other(TagError( return Err(StreamErrorFor::<Input>::other(TagError(
@ -135,6 +137,19 @@ where
}) })
} }
fn peek_closing_tag<Input>(tag: Tag) -> impl Parser<Input, Output = ()>
where
Input: Stream<Token = char>,
{
look_ahead(between(token('['), token(']'), tag_string())).and_then(move |closing_tag| {
if closing_tag == format!("/{}", tag) {
Ok(())
} else {
Err(StreamErrorFor::<Input>::other(TagError(closing_tag)))
}
})
}
fn closing_tag<Input>(tag: Tag) -> impl Parser<Input, Output = ()> fn closing_tag<Input>(tag: Tag) -> impl Parser<Input, Output = ()>
where where
Input: Stream<Token = char>, Input: Stream<Token = char>,
@ -273,25 +288,11 @@ where
satisfy(|c| c != '\n') satisfy(|c| c != '\n')
} }
fn char_node<Input>(closing: Option<Tag>) -> impl Parser<Input, Output = Node> fn char_node<Input>() -> impl Parser<Input, Output = Node>
where where
Input: Stream<Token = char>, Input: Stream<Token = char>,
{ {
if let Some(tag) = closing { valid_char().map(|text| Node::CharNode { text })
look_ahead(closing_tag(tag))
.map(|_| None)
.or(valid_char().map(Some))
.and_then(|text: Option<char>| {
if let Some(text) = text {
Ok(Node::CharNode { text })
} else {
Err(StreamErrorFor::<Input>::other(ClosingTagBackout))
}
})
.left()
} else {
valid_char().map(|text| Node::CharNode { text }).right()
}
} }
fn newline_node<Input>() -> impl Parser<Input, Output = Node> fn newline_node<Input>() -> impl Parser<Input, Output = Node>
@ -301,7 +302,7 @@ where
many1(combine::parser::char::char('\n')).map(|_: String| Node::NewlineNode) many1(combine::parser::char::char('\n')).map(|_: String| Node::NewlineNode)
} }
fn single_node<Input>(closing: Option<Tag>) -> impl Parser<Input, Output = Node> fn single_node_<Input>() -> impl Parser<Input, Output = Node>
where where
Input: Stream<Token = char>, Input: Stream<Token = char>,
{ {
@ -310,11 +311,32 @@ where
attempt(handle_node()), attempt(handle_node()),
attempt(email_node()), attempt(email_node()),
attempt(url_node()), attempt(url_node()),
char_node(closing), char_node(),
newline_node(), attempt(newline_node()),
)) ))
} }
fn single_node<Input>(closing: Option<Tag>) -> impl Parser<Input, Output = Node>
where
Input: Stream<Token = char>,
{
if let Some(tag) = closing {
peek_closing_tag(tag)
.map(|_| None)
.or(single_node_().map(Some))
.and_then(move |node: Option<Node>| {
if let Some(node) = node {
Ok(node)
} else {
Err(StreamErrorFor::<Input>::other(ClosingTagBackout))
}
})
.left()
} else {
single_node_().right()
}
}
fn node_vec_<Input>(closing: Option<Tag>) -> impl Parser<Input, Output = Vec<Node>> fn node_vec_<Input>(closing: Option<Tag>) -> impl Parser<Input, Output = Vec<Node>>
where where
Input: Stream<Token = char>, Input: Stream<Token = char>,
@ -354,6 +376,7 @@ impl std::fmt::Display for Tag {
Tag::IconText => "icontext", Tag::IconText => "icontext",
Tag::Icon => "icon", Tag::Icon => "icon",
Tag::Hr => "hr", Tag::Hr => "hr",
Tag::Br => "br",
Tag::Url => "url", Tag::Url => "url",
}; };
@ -399,6 +422,36 @@ mod tests {
} }
} }
#[test]
fn parse_node_with_bad_inner_tag() {
let (node, rest) = node_vec(None)
.easy_parse("[center][bold][/center]")
.unwrap();
assert_eq!(rest, "");
match &node[0] {
Node::TagNode { tag, children, .. } => {
assert_eq!(*tag, Tag::Center);
assert_eq!(children.len(), 6);
}
_ => panic!("Invalid node type"),
}
}
#[test]
fn parse_node_with_unknown_inner_tag() {
let (node, rest) = node_vec(None)
.easy_parse("[center][unknown][/unknown][/center]")
.unwrap();
assert_eq!(rest, "");
match &node[0] {
Node::TagNode { tag, children, .. } => {
assert_eq!(*tag, Tag::Center);
assert_eq!(children.len(), 19);
}
_ => panic!("Invalid node type"),
}
}
#[test] #[test]
fn parse_multiple_nodes() { fn parse_multiple_nodes() {
let (vec, rest) = node_vec(None) let (vec, rest) = node_vec(None)

View file

@ -73,6 +73,7 @@ static AMMONIA_CONFIG: Lazy<Builder> = Lazy::new(|| {
let div_hs = classes.entry("div").or_insert(HashSet::new()); let div_hs = classes.entry("div").or_insert(HashSet::new());
div_hs.insert("center"); div_hs.insert("center");
div_hs.insert("right"); div_hs.insert("right");
div_hs.insert("toolkit-code");
let span_hs = classes.entry("span").or_insert(HashSet::new()); let span_hs = classes.entry("span").or_insert(HashSet::new());
span_hs.insert("underline"); span_hs.insert("underline");
@ -82,6 +83,7 @@ static AMMONIA_CONFIG: Lazy<Builder> = Lazy::new(|| {
let pre_hs = classes.entry("pre").or_insert(HashSet::new()); let pre_hs = classes.entry("pre").or_insert(HashSet::new());
pre_hs.insert("codeblock"); pre_hs.insert("codeblock");
pre_hs.insert("toolkit-code--pre");
let mut schemes = HashSet::new(); let mut schemes = HashSet::new();
schemes.insert("http"); schemes.insert("http");
@ -104,6 +106,7 @@ static AMMONIA_CONFIG: Lazy<Builder> = Lazy::new(|| {
tags.insert("a"); tags.insert("a");
tags.insert("img"); tags.insert("img");
tags.insert("br"); tags.insert("br");
tags.insert("hr");
let mut builder = Builder::new(); let mut builder = Builder::new();
builder builder

View file

@ -95,8 +95,29 @@ enum RenderNode {
Newline, Newline,
} }
fn trim(mut nodes: Vec<RenderNode>) -> Vec<RenderNode> {
let mut pop_last = false;
let mut pop_first = false;
if let Some(last) = nodes.last() {
pop_last = matches!(last, RenderNode::Newline);
}
if let Some(first) = nodes.first() {
pop_first = matches!(first, RenderNode::Newline);
}
if pop_last {
nodes.pop();
}
if pop_first {
nodes.into_iter().skip(1).collect()
} else {
nodes
}
}
fn render_nodes(nodes: Vec<RenderNode>) -> String { fn render_nodes(nodes: Vec<RenderNode>) -> String {
nodes trim(nodes)
.into_iter() .into_iter()
.map(|node| { .map(|node| {
log::trace!("Rendering {:?}", node); log::trace!("Rendering {:?}", node);
@ -132,9 +153,9 @@ fn render_nodes(nodes: Vec<RenderNode>) -> String {
} }
Tag::Codeblock if !children.is_empty() => { Tag::Codeblock if !children.is_empty() => {
String::new() String::new()
+ "<pre class=\"codeblock\">" + "<div class=\"toolkit-code\"><pre class=\"toolkit-code--pre\">"
+ &render_nodes(children) + &render_nodes(children)
+ "</pre>" + "</pre></div>"
} }
Tag::Pre if !children.is_empty() => { Tag::Pre if !children.is_empty() => {
String::new() + "<pre>" + &render_nodes(children) + "</pre>" String::new() + "<pre>" + &render_nodes(children) + "</pre>"
@ -187,6 +208,7 @@ fn render_nodes(nodes: Vec<RenderNode>) -> String {
Tag::IconText if !children.is_empty() => render_nodes(children), Tag::IconText if !children.is_empty() => render_nodes(children),
Tag::Icon if !children.is_empty() => render_nodes(children), Tag::Icon if !children.is_empty() => render_nodes(children),
Tag::Hr => String::from("<hr>"), Tag::Hr => String::from("<hr>"),
Tag::Br => String::from("<br>"),
Tag::Url if !children.is_empty() => { Tag::Url if !children.is_empty() => {
if let Some(href) = attr { if let Some(href) = attr {
format!("<a href=\"{}\" rel=\"noopener noreferer nofollow\">", href) format!("<a href=\"{}\" rel=\"noopener noreferer nofollow\">", href)
@ -390,7 +412,6 @@ fn build_nodes(input: Vec<crate::bbcode::Node>) -> Vec<Node> {
let mut nodes = vec![]; let mut nodes = vec![];
for n in input { for n in input {
log::trace!("Building {:?}", n);
match n { match n {
crate::bbcode::Node::TagNode { crate::bbcode::Node::TagNode {
tag, tag,