Content: Fix closing tag lookahead, support [br]
This commit is contained in:
parent
84d5aa39cf
commit
f2552c794d
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue