hyaenidae/content/src/render.rs

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");
}
}