#![cfg(feature = "convert")]
#[cfg(test)]
mod test {
use germ::{
ast::{Ast, Node},
convert::{Target, from_ast, from_string},
};
#[cfg(feature = "macros")] use germ::{gemini_to_html, gemini_to_md};
#[test]
fn convert_from_string_to_html_single_line() {
assert_eq!(from_string("hi", &Target::HTML), "<p>hi</p>",);
}
#[test]
fn convert_from_string_to_html_multi_line() {
assert_eq!(from_string("hi\n# hi", &Target::HTML), "<p>hi</p><h1>hi</h1>",);
}
#[test]
fn html_escapes_content_in_every_node() {
let hostile = "<&>\"'".to_owned();
let ast = Ast::from_nodes(vec![
Node::Text(hostile.clone()),
Node::Heading { level: 1, text: hostile.clone() },
Node::List(vec![hostile.clone()]),
Node::Blockquote(hostile.clone()),
Node::PreformattedText { alt_text: None, text: hostile },
]);
assert_eq!(
from_ast(&ast, &Target::HTML),
"<p><&>"'</p><h1><&>"'</\
h1><ul><li><&>"'</li></ul><blockquote><&>&\
quot;'</blockquote><pre><&>"'</pre>",
);
}
#[test]
fn html_escapes_link_targets_and_labels() {
let ast = Ast::from_nodes(vec![Node::Link {
to: "/path?x=\"<&>".to_owned(),
text: Some("<script>alert('x')</script>".to_owned()),
}]);
assert_eq!(
from_ast(&ast, &Target::HTML),
"<a href=\"/path?x="<&>\"><script>alert('x'\
)</script></a><br>",
);
}
#[test]
fn html_renders_unsafe_links_as_text() {
for target in [
"javascript:alert(1)",
"JaVaScRiPt:alert(1)",
"data:text/html,<script>alert(1)</script>",
"java\nscript:alert(1)",
"vbscript:msgbox(1)",
"sftp://example.com/file",
] {
let ast = Ast::from_nodes(vec![Node::Link {
to: target.to_owned(),
text: Some("safe label".to_owned()),
}]);
assert_eq!(from_ast(&ast, &Target::HTML), "safe label<br>");
}
}
#[test]
fn html_escapes_raw_gemtext() {
assert_eq!(
from_string(
"<script>alert(1)</script>\n=> javascript:alert(2) click",
&Target::HTML
),
"<p><script>alert(1)</script></p>click<br>",
);
}
#[test]
fn html_keeps_safe_relative_and_absolute_links() {
for target in [
"/local",
"relative",
"#anchor",
"gemini://example.com",
"https://example.com",
] {
let ast = Ast::from_nodes(vec![Node::Link {
to: target.to_owned(),
text: None,
}]);
assert_eq!(
from_ast(&ast, &Target::HTML),
format!("<a href=\"{target}\">{target}</a><br>"),
);
}
}
#[cfg(feature = "macros")]
#[test]
fn convert_from_string_to_html_single_link_macro_expression() {
assert_eq!(
gemini_to_html!("=> /to hello !"),
"<a href=\"/to\">hello !</a><br>",
);
}
#[test]
fn convert_from_string_to_markdown_single_line() {
assert_eq!(from_string("hi", &Target::Markdown), "hi\n",);
}
#[test]
fn convert_from_string_to_markdown_multi_line() {
assert_eq!(from_string("hi\n# hi", &Target::Markdown), "hi\n# hi\n",);
}
#[test]
fn convert_from_string_to_markdown_single_link() {
assert_eq!(
from_string("=> /to hello !", &Target::Markdown),
"[hello \\!](/to)\n",
);
}
#[test]
fn markdown_escapes_literal_text_in_every_node() {
let literal = "<script>*bold* © 1. item ".to_owned();
let ast = Ast::from_nodes(vec![
Node::Text(literal.clone()),
Node::Heading { level: 1, text: literal.clone() },
Node::List(vec![literal.clone()]),
Node::Blockquote(literal),
]);
let escaped = r"\<script\>\*bold\* \© 1\. item \!\[image\](x\)";
assert_eq!(
from_ast(&ast, &Target::Markdown),
format!("{escaped}\n# {escaped}\n- {escaped}\n> {escaped}\n"),
);
}
#[test]
fn markdown_escapes_gemtext_lines_that_look_like_markdown_blocks() {
assert_eq!(
from_string(
"#### heading\n- item\n1. item\n===\n<script>",
&Target::Markdown
),
"\\#\\#\\#\\# heading\n\\- item\n1\\. item\n\\=\\=\\=\n\\<script\\>\n",
);
}
#[test]
fn markdown_preserves_leading_and_trailing_spaces_as_text() {
assert_eq!(
from_string(" indented\ntext ", &Target::Markdown),
"  indented\ntext  \n",
);
}
#[test]
fn markdown_fence_cannot_close_inside_preformatted_content() {
let ast = Ast::from_nodes(vec![Node::PreformattedText {
alt_text: Some("ruby`~~~\nunsafe".to_owned()),
text: "before\n~~~~\nafter".to_owned(),
}]);
assert_eq!(
from_ast(&ast, &Target::Markdown),
"~~~~~ ruby`~~~unsafe\nbefore\n~~~~\nafter\n~~~~~\n",
);
}
#[test]
fn markdown_escapes_link_labels_and_destinations() {
assert_eq!(
from_string(
"=> /docs/(a)?x=1&next=<tag> A [link] *wow* ©",
&Target::Markdown
),
"[A \\[link\\] \\*wow\\* \
\\©](/docs/\\(a\\)?x=1&next=%3Ctag%3E)\n"
);
assert_eq!(
from_string("=> /local", &Target::Markdown),
"[/local](/local)\n"
);
}
#[test]
fn markdown_renders_unsafe_links_as_text() {
let ast = Ast::from_nodes(vec![
Node::Link {
to: "javascript:alert(1)".to_owned(),
text: Some("click".to_owned()),
},
Node::Link {
to: "/bad\nline".to_owned(),
text: Some("evil\nlabel".to_owned()),
},
]);
assert_eq!(from_ast(&ast, &Target::Markdown), "click\nevil label\n");
}
#[test]
fn renderers_keep_unclosed_preformatted_content() {
assert_eq!(
from_string("```alt\n* item\n# heading", &Target::HTML),
"<pre>* item\n# heading</pre>"
);
assert_eq!(
from_string("```alt\n* item\n# heading", &Target::Markdown),
"~~~ alt\n* item\n# heading\n~~~\n"
);
assert_eq!(
from_string("```alt\ncode\n", &Target::HTML),
"<pre>code\n</pre>"
);
}
#[test]
fn markdown_separates_a_fenced_block_from_following_text() {
assert_eq!(
from_string("```\ncode\n```\nafter", &Target::Markdown),
"~~~\ncode\n~~~\nafter\n"
);
}
#[cfg(feature = "macros")]
#[test]
fn convert_from_string_to_markdown_single_macro_expression() {
assert_eq!(gemini_to_md!("=> /to hello !"), "[hello \\!](/to)\n",);
}
}