use crate::{
RenderOptions,
gemtext::{GemtextContentBlock, HeadingLevel},
};
use alloc::string::ToString;
use maud::{Markup, html};
impl GemtextContentBlock {
pub(crate) fn as_markup(&self, options: MarkupOptions) -> Markup {
match self {
#[rustfmt::skip]
Self::Heading { level: HeadingLevel::One, content } => html! { h1 { (content) } },
#[rustfmt::skip]
Self::Heading { level: HeadingLevel::Two, content } => html! { h2 { (content) } },
#[rustfmt::skip]
Self::Heading { level: HeadingLevel::Three, content } => html! { h3 { (content) } },
Self::Link { target, label }
if [".webp", ".png", ".apng", ".jpg", ".jpeg"]
.iter()
.any(|s| target.to_ascii_lowercase().ends_with(s)) =>
{
html! {
p {
details class="image" {
summary title=(target) { p { (label.as_ref().unwrap_or(target)) } }
img src=(target) alt="" loading="lazy";
}
}
}
}
Self::Link { target, label } => match url::Url::parse(target) {
Err(_) => html! { p { a href=(target) { (label.as_ref().unwrap_or(target)) } } }, Ok(url) => {
html!( p { a rel="external noopener noreferrer nofollow" href=(url) { (label.as_ref().unwrap_or(&url.to_string())) } } ) }
},
Self::List { items } => html! {
ul {
@for item in items {
li { (item) }
}
}
},
#[rustfmt::skip]
Self::Pre { alt_text: Some(alt), content } if alt.contains(" ") => html! {
figure aria-label=(alt) {
pre alt=(alt) { (content) }
}
},
#[rustfmt::skip]
Self::Pre { alt_text: Some(language_id), content } => match options.copy_button_style {
CopyButtonStyle::None => html! {
pre alt=(language_id) { (content) }
},
CopyButtonStyle::Forgejo => html! {
pre class="code-block" alt=(language_id) {
aside style="position:sticky; top:0; left:100%; height:0; width:0; overflow:visible" {
button class="code-copy ui button" style="top: -8px; right: -10px" data-clipboard-text=(content) {
svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-copy" width="16" height="16" aria-hidden="true" {
path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" {}
path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" {}
}
}
}
code style="white-space: pre" { (content) }
}
},
},
#[rustfmt::skip]
Self::Pre { alt_text: None, content } => html! { pre { (content) } },
Self::Quote { content } => html! { blockquote { (content) } },
Self::Text { content } if content.trim().is_empty() => match options.empty_line_tag {
EmptyLineTag::Br => html! { br; },
EmptyLineTag::P => html! { p {} },
},
Self::Text { content } => html! { p { (content) } },
}
}
}
#[derive(Clone, Copy, Default)]
pub(crate) struct MarkupOptions {
pub copy_button_style: CopyButtonStyle,
pub empty_line_tag: EmptyLineTag,
}
impl From<RenderOptions> for MarkupOptions {
fn from(value: RenderOptions) -> Self {
Self {
copy_button_style: value.copy_button_style,
empty_line_tag: value.empty_line_tag,
}
}
}
#[derive(Clone, Copy, Default)]
#[cfg_attr(feature = "std", derive(clap::ValueEnum), value(rename_all = "lower"))]
pub enum CopyButtonStyle {
#[default]
None,
Forgejo,
}
#[derive(Clone, Copy, Default)]
#[cfg_attr(feature = "std", derive(clap::ValueEnum), value(rename_all = "lower"))]
pub enum EmptyLineTag {
#[default]
Br,
P,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gemtext::Document;
use pretty_assertions::assert_eq;
#[test]
fn test_renders_preformatted_single_line() {
let pre = GemtextContentBlock::Pre {
alt_text: None,
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.".into(),
};
let expected = r#"<pre>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</pre>"#;
assert_eq!(pre.as_markup(Default::default()).into_string(), expected);
}
#[test]
fn test_renders_preformatted_multi_line() {
let pre = GemtextContentBlock::Pre {
alt_text: None,
content: r#"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."#.into(),
};
let expected = r#"<pre>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</pre>"#;
assert_eq!(pre.as_markup(Default::default()).into_string(), expected);
}
#[test]
fn test_renders_headings() {
let cases = [
("#Heading", "<h1>Heading</h1>"),
("# Heading", "<h1>Heading</h1>"),
("# Heading", "<h1>Heading</h1>"),
("# Heading ", "<h1>Heading</h1>"),
("# Heading ", "<h1>Heading</h1>"),
("##Heading", "<h2>Heading</h2>"),
("## Heading", "<h2>Heading</h2>"),
("## Heading", "<h2>Heading</h2>"),
("###Heading", "<h3>Heading</h3>"),
("### Heading", "<h3>Heading</h3>"),
("### Heading", "<h3>Heading</h3>"),
("### Heading", "<h3>Heading</h3>"),
];
for (test, expected) in cases {
let document = Document::parse_from_gemtext(test);
let lines = document.contents;
assert_eq!(lines.len(), 1);
let heading = lines.first().expect("single-line document");
let result = heading.as_markup(Default::default()).into_string();
assert_eq!(result, expected);
}
}
#[test]
fn test_renders_links() {
#[rustfmt::skip]
let cases = [
("=> test", r#"<p><a href="test">test</a></p>"#),
("=> test link", r#"<p><a href="test">link</a></p>"#),
("=> /foo", r#"<p><a href="/foo">/foo</a></p>"#),
("=> foo://bar", r#"<p><a rel="external noopener noreferrer nofollow" href="foo://bar">foo://bar</a></p>"#),
("=> foo://bar ext", r#"<p><a rel="external noopener noreferrer nofollow" href="foo://bar">ext</a></p>"#),
("=> foo://bar foo://baz", r#"<p><a rel="external noopener noreferrer nofollow" href="foo://bar">foo://baz</a></p>"#), ];
for (test, expected) in cases {
let document = Document::parse_from_gemtext(test);
let lines = document.contents;
assert_eq!(lines.len(), 1);
let heading = lines.first().expect("single-line document");
let result = heading.as_markup(Default::default()).into_string();
assert_eq!(result, expected);
}
}
}