555 lines
18 KiB
Rust
555 lines
18 KiB
Rust
use crate::bbcode::Tag;
|
|
use std::borrow::Cow;
|
|
|
|
#[derive(Debug)]
|
|
pub enum NodeView<'a> {
|
|
Tag {
|
|
tag: Tag,
|
|
attr: Option<Cow<'a, str>>,
|
|
},
|
|
Url {
|
|
href: Cow<'a, str>,
|
|
},
|
|
IconText {
|
|
handle: Cow<'a, str>,
|
|
domain: Cow<'a, str>,
|
|
img: Option<String>,
|
|
href: Option<String>,
|
|
},
|
|
Icon {
|
|
handle: Cow<'a, str>,
|
|
domain: Cow<'a, str>,
|
|
img: Option<String>,
|
|
href: Option<String>,
|
|
},
|
|
Handle {
|
|
handle: Cow<'a, str>,
|
|
domain: Cow<'a, str>,
|
|
href: Option<String>,
|
|
},
|
|
Email {
|
|
email: Cow<'a, str>,
|
|
},
|
|
Text {
|
|
text: Cow<'a, str>,
|
|
},
|
|
Newline,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum Node {
|
|
Tag {
|
|
tag: Tag,
|
|
attr: Option<String>,
|
|
children: Vec<Node>,
|
|
},
|
|
Url {
|
|
href: String,
|
|
},
|
|
Handle {
|
|
handle: String,
|
|
domain: String,
|
|
},
|
|
Email {
|
|
email: String,
|
|
},
|
|
Text {
|
|
text: String,
|
|
},
|
|
Newline,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum RenderNode {
|
|
Tag {
|
|
tag: Tag,
|
|
attr: Option<String>,
|
|
children: Vec<RenderNode>,
|
|
},
|
|
Url {
|
|
href: String,
|
|
},
|
|
IconText {
|
|
handle: String,
|
|
domain: String,
|
|
img: String,
|
|
href: String,
|
|
},
|
|
Icon {
|
|
handle: String,
|
|
domain: String,
|
|
img: String,
|
|
href: String,
|
|
},
|
|
Handle {
|
|
handle: String,
|
|
domain: String,
|
|
href: String,
|
|
},
|
|
Email {
|
|
email: String,
|
|
},
|
|
Text {
|
|
text: String,
|
|
},
|
|
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 {
|
|
trim(nodes)
|
|
.into_iter()
|
|
.map(|node| {
|
|
log::trace!("Rendering {:?}", node);
|
|
match node {
|
|
RenderNode::Tag {
|
|
tag,
|
|
attr,
|
|
children,
|
|
} => match tag {
|
|
Tag::Right if !children.is_empty() => {
|
|
String::new() + "<div class=\"right\">" + &render_nodes(children) + "</div>"
|
|
}
|
|
Tag::Center if !children.is_empty() => {
|
|
String::new()
|
|
+ "<div class=\"center\">"
|
|
+ &render_nodes(children)
|
|
+ "</div>"
|
|
}
|
|
Tag::Quote if !children.is_empty() => {
|
|
String::new() + "<blockquote>" + &render_nodes(children) + "</blockquote>"
|
|
}
|
|
Tag::Color if !children.is_empty() => {
|
|
if let Some(attr) = attr {
|
|
format!("<span style=\"color:{};\">", attr)
|
|
+ &render_nodes(children)
|
|
+ "</span>"
|
|
} else {
|
|
render_nodes(children)
|
|
}
|
|
}
|
|
Tag::Code if !children.is_empty() => {
|
|
String::new() + "<code>" + &render_nodes(children) + "</code>"
|
|
}
|
|
Tag::Codeblock if !children.is_empty() => {
|
|
String::new()
|
|
+ "<div class=\"toolkit-code\"><pre class=\"toolkit-code--pre\">"
|
|
+ &render_nodes(children)
|
|
+ "</pre></div>"
|
|
}
|
|
Tag::Pre if !children.is_empty() => {
|
|
String::new() + "<pre>" + &render_nodes(children) + "</pre>"
|
|
}
|
|
Tag::Mono if !children.is_empty() => {
|
|
String::new()
|
|
+ "<span class=\"monospace\">"
|
|
+ &render_nodes(children)
|
|
+ "</span>"
|
|
}
|
|
Tag::Sub if !children.is_empty() => {
|
|
String::new() + "<sub>" + &render_nodes(children) + "</sub>"
|
|
}
|
|
Tag::Sup if !children.is_empty() => {
|
|
String::new() + "<sup>" + &render_nodes(children) + "</sup>"
|
|
}
|
|
Tag::S if !children.is_empty() => {
|
|
String::new() + "<s>" + &render_nodes(children) + "</s>"
|
|
}
|
|
Tag::Spoiler if !children.is_empty() => {
|
|
String::new()
|
|
+ "<span class=\"spoiler\">"
|
|
+ &render_nodes(children)
|
|
+ "</span>"
|
|
}
|
|
Tag::Bold if !children.is_empty() => {
|
|
String::new() + "<b>" + &render_nodes(children) + "</b>"
|
|
}
|
|
Tag::Strong if !children.is_empty() => {
|
|
String::new() + "<strong>" + &render_nodes(children) + "</strong>"
|
|
}
|
|
Tag::I if !children.is_empty() => {
|
|
String::new() + "<i>" + &render_nodes(children) + "</i>"
|
|
}
|
|
Tag::Em if !children.is_empty() => {
|
|
String::new() + "<em>" + &render_nodes(children) + "</em>"
|
|
}
|
|
Tag::U if !children.is_empty() => {
|
|
String::new()
|
|
+ "<span class=\"underline\">"
|
|
+ &render_nodes(children)
|
|
+ "</span>"
|
|
}
|
|
Tag::Smcaps if !children.is_empty() => {
|
|
String::new()
|
|
+ "<span class=\"smallcaps\">"
|
|
+ &render_nodes(children)
|
|
+ "</span>"
|
|
}
|
|
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=\"{}\" target=\"_blank\" rel=\"noopener noreferer nofollow\">", href)
|
|
+ &render_nodes(children)
|
|
+ "</a>"
|
|
} else {
|
|
render_nodes(children)
|
|
}
|
|
}
|
|
_ => String::new(),
|
|
},
|
|
RenderNode::Url { href } => format!(
|
|
"<a href=\"{href}\" target=\"blank\" rel=\"noopener noreferer nofollow\">{href}</a>",
|
|
href = href
|
|
),
|
|
RenderNode::IconText {
|
|
handle,
|
|
domain,
|
|
img,
|
|
href,
|
|
} => {
|
|
format!(
|
|
"<a class=\"icontext\" href=\"{}\" rel=\"noopener noreferer nofollow\">",
|
|
href
|
|
) + &format!(
|
|
"<img src=\"{}\" title=\"@{handle}@{domain}\" alt=\"@{handle}@{domain}\" align=\"middle\" />",
|
|
img,
|
|
handle = handle,
|
|
domain = domain
|
|
) + "</a> " + &format!(
|
|
"<a class=\"icontext\" href=\"{}\" rel=\"noopener noreferer nofollow\">",
|
|
href
|
|
)
|
|
+ &format!("@{}@{}", handle, domain)
|
|
+ "</a>"
|
|
}
|
|
RenderNode::Icon {
|
|
handle,
|
|
domain,
|
|
img,
|
|
href,
|
|
} => {
|
|
format!(
|
|
"<a class=\"icon\" href=\"{}\" rel=\"noopener noreferer nofollow\">",
|
|
href
|
|
) + &format!(
|
|
"<img src=\"{}\" title=\"@{handle}@{domain}\" alt=\"@{handle}@{domain}\" align=\"middle\" />",
|
|
img,
|
|
handle = handle,
|
|
domain = domain
|
|
) + &format!("</a>")
|
|
}
|
|
RenderNode::Handle {
|
|
handle,
|
|
domain,
|
|
href,
|
|
} => {
|
|
format!("<a href=\"{}\" rel=\"noopener noreferer nofollow\">", href)
|
|
+ &format!("@{}@{}", handle, domain)
|
|
+ &format!("</a>")
|
|
}
|
|
RenderNode::Email { email } => {
|
|
format!("<a href=\"mailto:{email}\">{email}</a>", email = email)
|
|
}
|
|
RenderNode::Text { text } => text,
|
|
RenderNode::Newline => format!("<br>"),
|
|
}
|
|
})
|
|
.collect::<String>()
|
|
}
|
|
|
|
fn to_render<'b, F>(node: NodeView<'b>, children: Option<Vec<Node>>, f: F) -> RenderNode
|
|
where
|
|
for<'a> F: Fn(NodeView<'a>) -> NodeView<'a> + Copy,
|
|
{
|
|
match node {
|
|
NodeView::Tag { tag, attr } => RenderNode::Tag {
|
|
tag,
|
|
attr: attr.map(|a| a.to_string()),
|
|
children: map_nodes(children.unwrap_or(vec![]), f),
|
|
},
|
|
NodeView::Url { href } => RenderNode::Url {
|
|
href: href.to_string(),
|
|
},
|
|
NodeView::IconText {
|
|
handle,
|
|
domain,
|
|
img,
|
|
href,
|
|
} => match (img, href) {
|
|
(Some(img), Some(href)) => RenderNode::IconText {
|
|
handle: handle.to_string(),
|
|
domain: domain.to_string(),
|
|
img,
|
|
href,
|
|
},
|
|
(None, Some(href)) => RenderNode::Handle {
|
|
handle: handle.to_string(),
|
|
domain: domain.to_string(),
|
|
href,
|
|
},
|
|
_ => RenderNode::Text {
|
|
text: format!("@{}@{}", handle, domain),
|
|
},
|
|
},
|
|
NodeView::Icon {
|
|
handle,
|
|
domain,
|
|
img,
|
|
href,
|
|
} => match (img, href) {
|
|
(Some(img), Some(href)) => RenderNode::Icon {
|
|
handle: handle.to_string(),
|
|
domain: domain.to_string(),
|
|
img,
|
|
href,
|
|
},
|
|
(None, Some(href)) => RenderNode::Handle {
|
|
handle: handle.to_string(),
|
|
domain: domain.to_string(),
|
|
href,
|
|
},
|
|
_ => RenderNode::Text {
|
|
text: format!("@{}@{}", handle, domain),
|
|
},
|
|
},
|
|
NodeView::Handle {
|
|
handle,
|
|
domain,
|
|
href,
|
|
} => match href {
|
|
Some(href) => RenderNode::Handle {
|
|
handle: handle.to_string(),
|
|
domain: domain.to_string(),
|
|
href: href.to_string(),
|
|
},
|
|
None => RenderNode::Text {
|
|
text: format!("@{}@{}", handle, domain),
|
|
},
|
|
},
|
|
NodeView::Email { email } => RenderNode::Email {
|
|
email: email.to_string(),
|
|
},
|
|
NodeView::Text { text } => RenderNode::Text {
|
|
text: text.to_string(),
|
|
},
|
|
NodeView::Newline => RenderNode::Newline,
|
|
}
|
|
}
|
|
|
|
fn map_nodes<F>(nodes: Vec<Node>, f: F) -> Vec<RenderNode>
|
|
where
|
|
for<'a> F: Fn(NodeView<'a>) -> NodeView<'a> + Copy,
|
|
{
|
|
nodes
|
|
.into_iter()
|
|
.map(move |node| {
|
|
log::trace!("Mapping {:?}", node);
|
|
match node {
|
|
Node::Tag {
|
|
tag,
|
|
attr,
|
|
children,
|
|
} => match tag {
|
|
Tag::IconText => match children.get(0) {
|
|
Some(Node::Handle { handle, domain }) => to_render(
|
|
(f)(NodeView::IconText {
|
|
handle: Cow::Borrowed(&handle),
|
|
domain: Cow::Borrowed(&domain),
|
|
href: None,
|
|
img: None,
|
|
}),
|
|
None,
|
|
f,
|
|
),
|
|
_ => to_render(
|
|
(f)(NodeView::Tag {
|
|
tag,
|
|
attr: attr.as_deref().map(Cow::Borrowed),
|
|
}),
|
|
Some(children),
|
|
f,
|
|
),
|
|
},
|
|
Tag::Icon => match children.get(0) {
|
|
Some(Node::Handle { handle, domain }) => to_render(
|
|
(f)(NodeView::Icon {
|
|
handle: Cow::Borrowed(&handle),
|
|
domain: Cow::Borrowed(&domain),
|
|
href: None,
|
|
img: None,
|
|
}),
|
|
None,
|
|
f,
|
|
),
|
|
_ => to_render(
|
|
(f)(NodeView::Tag {
|
|
tag,
|
|
attr: attr.as_deref().map(Cow::Borrowed),
|
|
}),
|
|
Some(children),
|
|
f,
|
|
),
|
|
},
|
|
tag => to_render(
|
|
(f)(NodeView::Tag {
|
|
tag,
|
|
attr: attr.as_deref().map(Cow::Borrowed),
|
|
}),
|
|
Some(children),
|
|
f,
|
|
),
|
|
},
|
|
Node::Url { href } => to_render(
|
|
(f)(NodeView::Url {
|
|
href: Cow::Borrowed(&href),
|
|
}),
|
|
None,
|
|
f,
|
|
),
|
|
Node::Handle { handle, domain } => to_render(
|
|
(f)(NodeView::Handle {
|
|
handle: Cow::Borrowed(&handle),
|
|
domain: Cow::Borrowed(&domain),
|
|
href: None,
|
|
}),
|
|
None,
|
|
f,
|
|
),
|
|
Node::Email { email } => to_render(
|
|
(f)(NodeView::Email {
|
|
email: Cow::Borrowed(&email),
|
|
}),
|
|
None,
|
|
f,
|
|
),
|
|
Node::Text { text } => to_render(
|
|
(f)(NodeView::Text {
|
|
text: Cow::Borrowed(&text),
|
|
}),
|
|
None,
|
|
f,
|
|
),
|
|
Node::Newline => to_render((f)(NodeView::Newline), None, f),
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn build_nodes(input: Vec<crate::bbcode::Node>) -> Vec<Node> {
|
|
let mut nodes = vec![];
|
|
|
|
for n in input {
|
|
match n {
|
|
crate::bbcode::Node::TagNode {
|
|
tag,
|
|
attr,
|
|
children,
|
|
} => nodes.push(Node::Tag {
|
|
tag,
|
|
attr,
|
|
children: build_nodes(children),
|
|
}),
|
|
crate::bbcode::Node::UrlNode { url } => nodes.push(Node::Url {
|
|
href: url.to_string(),
|
|
}),
|
|
crate::bbcode::Node::HandleNode { handle, domain } => {
|
|
nodes.push(Node::Handle { handle, domain })
|
|
}
|
|
crate::bbcode::Node::EmailNode { email } => nodes.push(Node::Email { email }),
|
|
crate::bbcode::Node::CharNode { text: c_text } => match nodes.last_mut() {
|
|
Some(Node::Text { ref mut text }) => {
|
|
text.push(c_text);
|
|
}
|
|
_ => {
|
|
let mut text = String::new();
|
|
text.push(c_text);
|
|
nodes.push(Node::Text { text });
|
|
}
|
|
},
|
|
crate::bbcode::Node::NewlineNode => nodes.push(Node::Newline),
|
|
};
|
|
}
|
|
|
|
nodes
|
|
}
|
|
|
|
pub(crate) fn preprocessor<F>(source: &str, mapper: F) -> String
|
|
where
|
|
for<'a> F: Fn(NodeView<'a>) -> NodeView<'a> + Copy,
|
|
{
|
|
use combine::Parser;
|
|
|
|
let parsenodes = crate::bbcode::node_vec(None)
|
|
.parse(source)
|
|
.ok()
|
|
.map(|(nodes, rest)| {
|
|
if rest.len() > 0 {
|
|
log::warn!("Failed to parse '{}', rest: '{}'", source, rest);
|
|
}
|
|
|
|
nodes
|
|
})
|
|
.unwrap_or(vec![]);
|
|
|
|
render_nodes(map_nodes(build_nodes(parsenodes), mapper))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::preprocessor;
|
|
|
|
#[test]
|
|
fn basic_parse() {
|
|
let input = "some plain text";
|
|
let output = preprocessor(input, |view| view);
|
|
assert_eq!(output, input)
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_link() {
|
|
let input = "it's http://example.com a link";
|
|
let output = preprocessor(input, |view| view);
|
|
assert_eq!(output, "it's <a href=\"http://example.com\" rel=\"noopener noreferer nofollow\">http://example.com</a> a link");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_custom_link() {
|
|
let input = "it's [url=http://example.com]a link[/url]";
|
|
let output = preprocessor(input, |view| view);
|
|
assert_eq!(
|
|
output,
|
|
"it's <a href=\"http://example.com\" rel=\"noopener noreferer nofollow\">a link</a>"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_strong() {
|
|
let input = "it's [strong]bold[/strong] right";
|
|
let output = preprocessor(input, |view| view);
|
|
assert_eq!(output, "it's <strong>bold</strong> right");
|
|
}
|
|
}
|