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,
Icon,
Hr,
Br,
Url,
}
@ -68,7 +69,7 @@ struct ClosingTagBackout;
impl Tag {
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> {
@ -121,6 +122,7 @@ where
"icontext" => Tag::IconText,
"icon" => Tag::Icon,
"hr" => Tag::Hr,
"br" => Tag::Br,
"url" => Tag::Url,
_ => {
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 = ()>
where
Input: Stream<Token = char>,
@ -273,25 +288,11 @@ where
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
Input: Stream<Token = char>,
{
if let Some(tag) = closing {
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()
}
valid_char().map(|text| Node::CharNode { text })
}
fn newline_node<Input>() -> impl Parser<Input, Output = Node>
@ -301,7 +302,7 @@ where
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
Input: Stream<Token = char>,
{
@ -310,11 +311,32 @@ where
attempt(handle_node()),
attempt(email_node()),
attempt(url_node()),
char_node(closing),
newline_node(),
char_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>>
where
Input: Stream<Token = char>,
@ -354,6 +376,7 @@ impl std::fmt::Display for Tag {
Tag::IconText => "icontext",
Tag::Icon => "icon",
Tag::Hr => "hr",
Tag::Br => "br",
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]
fn parse_multiple_nodes() {
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());
div_hs.insert("center");
div_hs.insert("right");
div_hs.insert("toolkit-code");
let span_hs = classes.entry("span").or_insert(HashSet::new());
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());
pre_hs.insert("codeblock");
pre_hs.insert("toolkit-code--pre");
let mut schemes = HashSet::new();
schemes.insert("http");
@ -104,6 +106,7 @@ static AMMONIA_CONFIG: Lazy<Builder> = Lazy::new(|| {
tags.insert("a");
tags.insert("img");
tags.insert("br");
tags.insert("hr");
let mut builder = Builder::new();
builder

View file

@ -95,8 +95,29 @@ enum RenderNode {
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 {
nodes
trim(nodes)
.into_iter()
.map(|node| {
log::trace!("Rendering {:?}", node);
@ -132,9 +153,9 @@ fn render_nodes(nodes: Vec<RenderNode>) -> String {
}
Tag::Codeblock if !children.is_empty() => {
String::new()
+ "<pre class=\"codeblock\">"
+ "<div class=\"toolkit-code\"><pre class=\"toolkit-code--pre\">"
+ &render_nodes(children)
+ "</pre>"
+ "</pre></div>"
}
Tag::Pre if !children.is_empty() => {
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::Icon if !children.is_empty() => render_nodes(children),
Tag::Hr => String::from("<hr>"),
Tag::Br => String::from("<br>"),
Tag::Url if !children.is_empty() => {
if let Some(href) = attr {
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![];
for n in input {
log::trace!("Building {:?}", n);
match n {
crate::bbcode::Node::TagNode {
tag,