use pest::{iterators::Pair, Parser};
use crate::{
code::convert_code_to_html, safety::html::unsafe_string, ContextParser, ContextParserError,
Rule,
};
fn convert_inner_pair_to_html(pair: Pair<'_, Rule>) -> Option<String> {
pair.into_inner()
.next()
.and_then(|pair| convert_pair_to_html(pair).ok())
.and_then(|converted| converted)
}
fn convert_list_to_html(pair: Pair<'_, Rule>) -> Option<String> {
let is_nested = match pair.as_rule() {
Rule::List => false,
Rule::NestedList => true,
_ => unreachable!("Only list rules are handled here"),
};
let mut pairs = pair.into_inner();
let Some(indentation) = pairs
.next()
.and_then(|pair| convert_pair_to_html(pair).ok())
.and_then(|pair| pair)
else {
return None;
};
let items = pairs
.filter_map(|pair| convert_pair_to_html(pair).unwrap_or_default())
.map(|item| format!("<span>{indentation}{item}</span>"))
.collect::<Vec<String>>()
.join(" ");
let suffix = if is_nested { "" } else { " " };
Some(format!("<span class=\"list\">{}</span>{}", items, suffix))
}
fn convert_pair_to_html(pair: Pair<'_, Rule>) -> Result<Option<String>, ContextParserError> {
Ok(Some(match pair.as_rule() {
Rule::IgnoredUnicode | Rule::EOI => return Ok(None),
Rule::Block
| Rule::Span
| Rule::Text
| Rule::BlockContent
| Rule::MentionBoundary
| Rule::SlashLinkBoundary
| Rule::HashTagBoundary
| Rule::QuoteBoundary
| Rule::CodeBoundary
| Rule::InlineCodeBoundary
| Rule::ListItemBoundary
| Rule::WikiLinkOpenBoundary
| Rule::WikiLinkCloseBoundary
| Rule::LeadingInlineBoundary
| Rule::Bridge
| Rule::BridgeCharacter
| Rule::TextCharacter
| Rule::NonTextSpan
| Rule::Contiguous
| Rule::Terminator
| Rule::Whitespace
| Rule::Indentation
| Rule::SlugSpecialCharacter
| Rule::Protocol
| Rule::Context => unsafe_string(pair.as_str()).into(),
Rule::Empty => r#"<span class="empty"> </span>"#.into(),
Rule::Slug => format!(
"<span class=\"slug\">{}</span>",
unsafe_string(pair.as_str())
),
Rule::Petname => format!(
"<span class=\"petname\">{}</span>",
unsafe_string(pair.as_str())
),
Rule::HyperLink => format!(
"<span class=\"hyperlink\"><a href=\"{0}\" target=\"_blank\">{0}</a></span>",
unsafe_string(pair.as_str())
),
Rule::Mention => {
let Some(petname) = convert_inner_pair_to_html(pair) else {
return Ok(None);
};
format!("<span class=\"mention\">@{}</span>", petname)
}
Rule::SlashLink => {
let mut pairs = pair.into_inner();
let mut mention = None;
let mut link = None;
while let Some(pair) = pairs.next() {
match pair.as_rule() {
Rule::Mention => {
mention = convert_pair_to_html(pair)?;
}
Rule::Slug => link = convert_pair_to_html(pair)?,
_ => return Ok(None),
}
}
link.map(|link| {
format!(
"<span class=\"slashlink\">{}/{}</span>",
mention.unwrap_or_default(),
link
)
})
.unwrap_or_default()
}
Rule::HashTag => {
let Some(slug) = convert_inner_pair_to_html(pair) else {
return Ok(None);
};
format!("<span class=\"hashtag\">#{}</span>", slug)
}
Rule::InlineCode => {
let Some(value) = convert_inner_pair_to_html(pair) else {
return Ok(None);
};
format!("<span class=\"inlinecode\"><span class=\"fence\">`</span>{}<span class=\"fence\">`</span></span>", value)
}
Rule::InlineCodeValue => {
format!(
"<span class=\"inlinecodevalue\">{}</span>",
unsafe_string(pair.as_str())
)
}
Rule::WikiLink => {
let Some(value) = convert_inner_pair_to_html(pair) else {
return Ok(None);
};
format!("<span class=\"wikilink\">[[{}]]</span>", value)
}
Rule::WikiLinkValue => {
format!(
"<span class=\"wikilinkvalue\">{}</span>",
unsafe_string(pair.as_str())
)
}
Rule::Paragraph => {
let content = pair
.into_inner()
.map(|pair| {
convert_pair_to_html(pair)
.unwrap_or_default()
.unwrap_or_default()
})
.collect::<Vec<String>>()
.concat();
format!("<span class=\"paragraph\">{} </span>", content)
}
Rule::Code => {
let mut pairs = pair.into_inner();
let mut slug = None;
let mut kind = None;
let mut code_value = None;
while let Some(pair) = pairs.next() {
match pair.as_rule() {
Rule::Slug => {
kind = Some(pair.as_str().to_owned());
slug = convert_pair_to_html(pair)?;
}
Rule::CodeValue => {
code_value = if let Some(kind) = kind.as_ref() {
let code = pair.as_str();
Some(
convert_code_to_html(kind, code)
.unwrap_or_else(|| unsafe_string(code).to_string()),
)
} else {
Some(unsafe_string(pair.as_str()).to_string())
};
}
_ => return Ok(None),
}
}
format!(
"<span class=\"code\"><span class=\"fence\">```{}</span> {}<span class=\"fence\">```</span> </span>",
slug.unwrap_or_default(),
code_value
.unwrap_or_default()
)
}
Rule::CodeValue => {
format!(
"<span class=\"codevalue\">{}</span>",
unsafe_string(pair.as_str())
)
}
Rule::List | Rule::NestedList => convert_list_to_html(pair).unwrap_or_default(),
Rule::ListIndentation => {
if pair.as_str().len() == 0 {
String::new()
} else {
format!(
"<span class=\"listidentation\">{}</span>",
pair.as_str()
.chars()
.map(|_| " ")
.collect::<Vec<&str>>()
.concat()
)
}
}
Rule::ListItem => {
let mut pairs = pair.into_inner();
let Some(content) = pairs
.next()
.and_then(|pair| convert_pair_to_html(pair).ok())
.and_then(|pair| pair)
else {
return Ok(None);
};
let sublist = if let Some(pair) = pairs.next() {
convert_pair_to_html(pair)
.ok()
.and_then(|pair| pair)
.map(|sublist| format!(" {}", sublist))
} else {
None
};
format!(
"<span class=\"listitem\"><span class=\"listbullet\">- </span>{content}{}</span>",
sublist.unwrap_or_default()
)
}
Rule::ListItemContent => {
let content = pair
.into_inner()
.filter_map(|pair| convert_pair_to_html(pair).ok().unwrap_or_default())
.collect::<Vec<String>>()
.concat();
format!("<span class=\"listitemcontent\">{content}</span>")
}
Rule::Quote => {
let lines = pair
.into_inner()
.filter_map(|pair| convert_pair_to_html(pair).unwrap_or_default())
.collect::<Vec<String>>()
.concat();
format!("<span class=\"quote\">{lines}</span>")
}
Rule::QuoteLine => {
let content = pair
.into_inner()
.filter_map(|pair| convert_pair_to_html(pair).ok().unwrap_or_default())
.collect::<Vec<String>>()
.concat();
format!("<span class=\"quoteline\">> <span class=\"quotelinecontent\">{content}</span></span> ")
}
}))
}
pub fn convert_block_to_html(value: &str) -> Result<String, ContextParserError> {
let mut root = ContextParser::parse(Rule::Block, value)
.map_err(|error| ContextParserError::ParseError(format!("{error}")))?;
if let Some(pair) = root.next() {
Ok(convert_pair_to_html(pair)?.unwrap_or_default())
} else {
Ok(String::new())
}
}
pub fn convert_document_to_html(value: &str) -> Result<Vec<String>, ContextParserError> {
let root = ContextParser::parse(Rule::Context, value)
.map_err(|error| ContextParserError::ParseError(format!("{error}")))?;
let mut html = Vec::<String>::new();
for context in root {
for block in context.into_inner() {
if let Some(html_chunk) = convert_pair_to_html(block)? {
html.push(html_chunk);
}
}
}
Ok(html)
}
#[cfg(test)]
mod tests {
use crate::html::{convert_block_to_html, convert_document_to_html};
#[test]
fn it_converts_a_basic_paragraph_to_html() {
let html = convert_block_to_html("Hello, world!").unwrap();
assert_eq!(html, "<span class=\"paragraph\">Hello, world! </span>");
}
#[test]
fn it_converts_multiple_basic_paragraphs_to_html() {
let html = convert_document_to_html("Hello, \nworld!").unwrap();
assert_eq!(
html,
vec![
"<span class=\"paragraph\">Hello, </span>",
"<span class=\"paragraph\">world! </span>"
]
)
}
#[test]
fn it_converts_a_complex_paragraph_to_html() {
let html = convert_block_to_html(
"Hello, world! This #paragraph contains /interesting/content. [[Cool Stuff]].",
)
.unwrap();
assert_eq!(
html,
"<span class=\"paragraph\">Hello, world! This <span class=\"hashtag\">#<span class=\"slug\">paragraph</span></span> contains <span class=\"slashlink\">/<span class=\"slug\">interesting/content</span></span>. <span class=\"wikilink\">[[<span class=\"wikilinkvalue\">Cool Stuff</span>]]</span>. </span>"
);
}
#[test]
fn it_converts_a_basic_list_to_html() {
let html = convert_block_to_html(
r#"- foo
- bar
- baz"#,
)
.unwrap();
assert_eq!(
html,
"<span class=\"list\"><span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">foo</span></span></span> <span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">bar</span></span></span> <span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">baz</span></span></span></span> "
);
}
#[test]
fn it_converts_a_basic_nested_list_to_html() {
let html = convert_block_to_html(
r#"- foo
- bar
- baz"#,
)
.unwrap();
assert_eq!(
html,
"<span class=\"list\"><span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">foo</span> <span class=\"list\"><span><span class=\"listidentation\"> </span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">bar</span></span></span></span></span></span> <span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">baz</span></span></span></span> "
);
}
#[test]
fn it_converts_the_project_example_to_html() {
let html = convert_document_to_html(include_str!("../example.context")).unwrap();
println!("{:#?}", html);
assert_eq!(html, vec![
"<span class=\"paragraph\">This is a paragraph. It can contain <span class=\"slashlink\">/<span class=\"slug\">slashlinks</span></span>. It may contain <span class=\"mention\">@<span class=\"petname\">mentions</span></span> and so <span class=\"slashlink\"><span class=\"mention\">@<span class=\"petname\">may</span></span>/<span class=\"slug\">slash/links</span></span>. It may also contain <span class=\"hashtag\">#<span class=\"slug\">hashtags</span></span>. It may also contain <span class=\"hyperlink\"><a href=\"https://hyper.links\" target=\"_blank\">https://hyper.links</a></span>. It may also contain <span class=\"wikilink\">[[<span class=\"wikilinkvalue\">wiki-style links</span>]]</span>. It may also contain <span class=\"inlinecode\">`<span class=\"inlinecodevalue\">code blocks</span>`</span>. </span>",
"<span class=\"empty\"> </span>",
"<span class=\"list\"><span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">This is a list</span></span></span> <span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">It can have several items</span> <span class=\"list\"><span><span class=\"listidentation\"> </span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">The items can have nested lists</span></span></span> <span><span class=\"listidentation\"> </span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">With multiple items</span> <span class=\"list\"><span><span class=\"listidentation\"> </span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">They can be nested as deep as you like</span></span></span></span></span></span></span></span></span> <span><span class=\"listitem\"><span class=\"listbullet\">- </span><span class=\"listitemcontent\">With <span class=\"hashtag\">#<span class=\"slug\">all</span></span> the <span class=\"slashlink\">/<span class=\"slug\">same</span></span> <span class=\"slashlink\"><span class=\"mention\">@<span class=\"petname\">content</span></span>/<span class=\"slug\">types</span></span> as <span class=\"hyperlink\"><a href=\"http://a.paragraph\" target=\"_blank\">http://a.paragraph</a></span></span></span></span></span> ",
"<span class=\"empty\"> </span>",
"<span class=\"quote\"><span class=\"quoteline\">> <span class=\"quotelinecontent\">This is a quote block</span></span> <span class=\"quoteline\">> <span class=\"quotelinecontent\">It may be spread across several lines</span></span> <span class=\"quoteline\">> <span class=\"quotelinecontent\">and may include <span class=\"hashtag\">#<span class=\"slug\">the</span></span> <span class=\"mention\">@<span class=\"petname\">same</span></span> <span class=\"slashlink\"><span class=\"mention\">@<span class=\"petname\">kinds</span></span>/<span class=\"slug\">of</span></span> <span class=\"hyperlink\"><a href=\"https://content.as/a?paragraph\" target=\"_blank\">https://content.as/a?paragraph</a></span></span></span> </span>",
"<span class=\"empty\"> </span>",
"<span class=\"code\"><span class=\"fence\">```</span> // This is a code block\n// It can contain arbitrary text\n// spread across many lines\nprint("Hi!");\n<span class=\"fence\">```</span></span>",
"<span class=\"empty\"> </span>",
"<span class=\"code\"><span class=\"fence\">```<span class=\"slug\">rust</span></span> <span class=\"comment\">// A code block may be tagged</span> <span class=\"keyword\">fn</span> <span class=\"function\">main</span>() { <span class=\"macro\">println!</span>(<span class=\"string\">"Hi!"</span>); } <span class=\"fence\">```</span></span>",
]);
}
}