use super::*;
use crate::{Html, trusted_into};
fn basic(s: &str) -> String {
BasicHtml::new(s).into_string()
}
#[test]
fn the_allowed_tags_are_kept() {
let html = "<p><b>a</b> <i>b</i> <em>c</em> <strong>d</strong> <code>e</code><br>f</p><ul><li>g</li></ul><ol><li>h</li></ol>";
assert_eq!(basic(html), html);
}
#[test]
fn text_is_escaped() {
assert_eq!(
basic(r#"a "b" 'c' > d"#),
"a "b" 'c' > d"
);
}
#[test]
fn script_elements_stay_text() {
assert_eq!(
basic("<script>alert(1)</script>"),
"<script>alert(1)</script>"
);
assert_eq!(
basic("<scr<b>ipt>alert(1)</scr</b>ipt>"),
"<scr<b>ipt>alert(1)</scr</b>ipt>"
);
}
#[test]
fn allowed_tags_with_attributes_or_spaces_stay_text() {
for tag in [
r#"<b onclick="alert(1)">"#,
"<b onclick=alert(1)>",
"<b >",
"<b/>",
"<br/>",
"<br />",
"< b>",
"<b\n>",
"<b\0>",
r#"<a href="javascript:alert(1)">"#,
"<img src=x onerror=alert(1)>",
] {
let out = basic(tag);
assert!(out.starts_with("<"), "{tag}: {out}");
assert!(!out.contains('<'), "{tag}: {out}");
}
}
#[test]
fn other_cases_stay_text() {
assert_eq!(
basic("<B>a</B><Em>b</eM>"),
"<B>a</B><Em>b</eM>"
);
}
#[test]
fn references_stay_text() {
assert_eq!(basic("<b>x</b>"), "<b>x</b>");
assert_eq!(
basic("<b> é <"),
"<b> é <"
);
}
#[test]
fn ampersands_that_start_no_reference_are_escaped() {
assert_eq!(
basic("a & b &; &# &#; &#x; &1; < � � &"),
"a & b &; &# &#; &#x; &1; &lt &#12345678; &#x1234567; &amp"
);
let long = format!("&{};", "a".repeat(33));
assert_eq!(basic(&long), format!("&{}", &long[1..]));
}
#[test]
fn elements_left_open_are_closed() {
assert_eq!(basic("<b><i>a"), "<b><i>a</i></b>");
assert_eq!(basic("<ul><li>a<li>b"), "<ul><li>a</li><li>b</li></ul>");
}
#[test]
fn stray_end_tags_are_left_out() {
assert_eq!(basic("</p>a</b></li></ul>b"), "ab");
assert_eq!(basic("</br>"), "</br>");
}
#[test]
fn misnested_tags_are_closed_in_order() {
assert_eq!(basic("<b><i>a</b>b</i>"), "<b><i>a</i></b>b");
assert_eq!(basic("<p>a<p>b"), "<p>a</p><p>b</p>");
assert_eq!(basic("<b>a<p>b</p>c</b>"), "<b>a</b><p>b</p>c");
assert_eq!(
basic("<p>a<ul><li>b</ul>c</p>"),
"<p>a</p><ul><li>b</li></ul>c"
);
assert_eq!(
basic("<ul><li>a<ol><li>b<li>c</ol>d<li>e</ul>"),
"<ul><li>a<ol><li>b</li><li>c</li></ol>d</li><li>e</li></ul>"
);
assert_eq!(
basic("<ul><li><b>a<li>b"),
"<ul><li><b>a</b></li><li>b</li></ul>"
);
}
#[test]
fn a_list_item_outside_a_list_stays_text() {
assert_eq!(basic("<li>a</li>"), "<li>a");
}
#[test]
fn nesting_stops_at_the_depth_limit() {
let out = basic(&"<b>".repeat(40));
assert_eq!(out.matches("<b>").count(), MAX_DEPTH);
assert_eq!(out.matches("<b>").count(), 40 - MAX_DEPTH);
assert_eq!(out.matches("</b>").count(), MAX_DEPTH);
}
#[test]
fn nul_characters_are_left_out_and_line_breaks_are_newlines() {
assert_eq!(basic("a\0b<\0b>c\r\nd\re"), "ab<b>c\nd\ne");
}
#[test]
fn very_long_input_is_built_in_one_pass() {
let input = "<b>x</b><scr<i>ipt>&</i></p>\r\n".repeat(100_000);
assert_eq!(
basic(&input),
"<b>x</b><scr<i>ipt>&</i>\n".repeat(100_000)
);
let deep = "<i>".repeat(100_000);
assert!(basic(&deep).ends_with(&format!("<i>{}", "</i>".repeat(MAX_DEPTH))));
}
#[test]
fn building_again_gives_the_same_html() {
for input in [
"<b>a & b</b><p>c<ul><li>d",
"<b><li>\r<B>",
"<ul><li>a<ol><li>b<li>c</ol>d<li>e</ul>",
] {
let once = basic(input);
assert_eq!(basic(&once), once, "{input}");
}
}
#[test]
fn text_becomes_paragraphs_and_line_breaks() {
assert_eq!(
BasicHtml::from_text("\n\nOne\ntwo <b>\r\n \r\n\n& three\0\n").as_str(),
"<p>One<br>two <b></p><p>& three</p>"
);
assert_eq!(BasicHtml::from_text(" \n\n").as_str(), "");
}
#[test]
fn text_reads_back_as_itself() {
let text = BasicHtml::from_text("a <i>\n\nb & c");
assert_eq!(BasicHtml::new(text.as_str()), text);
}
#[test]
fn v_html_writes_the_string_unchanged() {
let html = BasicHtml::new("<b>a</b><img src=x>");
let mut out = String::new();
trusted_into(&mut out, &html);
assert_eq!(out, "<b>a</b><img src=x>");
}
#[derive(Serialize)]
struct Props {
body: BasicHtml,
}
fn render(out: &mut String, props: &Props) {
out.push_str("<article>");
trusted_into(out, &props.body);
out.push_str("</article>");
}
#[test]
fn an_island_carries_the_string_it_rendered() {
let props = Props {
body: BasicHtml::new("<b>a</b> & <i>b"),
};
assert_eq!(
Html::island("Note", &props, render).into_string(),
r#"<div data-island="Note" data-props="{"body":"<b>a</b> &amp; <i>b</i>"}"><article><b>a</b> & <i>b</i></article></div>"#
);
let json = serde_json::to_value(&props).unwrap();
assert_eq!(json["body"], props.body.as_str());
}
#[test]
fn deserialising_builds_it_again() {
let read: BasicHtml = serde_json::from_str(r#""<b onclick=x>a</b><i>b""#).unwrap();
assert_eq!(read.as_str(), "<b onclick=x>a<i>b</i>");
let again: BasicHtml = serde_json::from_str(&serde_json::to_string(&read).unwrap()).unwrap();
assert_eq!(again, read);
}
#[test]
fn the_tags_are_the_ones_it_keeps() {
for name in BasicHtml::TAGS {
let tag = Tag::named(name.as_bytes()).unwrap();
assert_eq!(tag.name(), name);
}
}
fn inline(s: &str) -> String {
InlineHtml::new(s).into_string()
}
#[test]
fn inline_html_keeps_the_inline_tags() {
let html = "<b>a</b> <i>b</i> <em>c</em> <strong>d</strong> <code>e</code><br>f";
assert_eq!(inline(html), html);
}
#[test]
fn inline_html_writes_block_tags_as_text() {
assert_eq!(
inline("<p>a</p><ul><li>b</li></ul><ol></ol>"),
"<p>a</p><ul><li>b</li></ul><ol></ol>"
);
assert_eq!(inline("<b>a<p>b</b>"), "<b>a<p>b</b>");
}
#[test]
fn inline_html_escapes_balances_and_reads_text_as_basic_html_does() {
assert_eq!(
inline("<b><i>a</b>b</i> & & </br>\0\r\n<B>"),
"<b><i>a</i></b>b & & </br>\n<B>"
);
let out = inline(&"<i>".repeat(40));
assert_eq!(out.matches("<i>").count(), MAX_DEPTH);
}
#[test]
fn inline_html_reads_back_as_itself() {
let read: InlineHtml = serde_json::from_str(r#""<b>a</b><p>b""#).unwrap();
assert_eq!(read.as_str(), "<b>a</b><p>b");
assert_eq!(InlineHtml::new(read.as_str()), read);
assert_eq!(
serde_json::to_string(&read).unwrap(),
r#""<b>a</b><p>b""#
);
let mut out = String::new();
trusted_into(&mut out, &read);
assert_eq!(out, read.as_str());
}
#[test]
fn the_inline_tags_are_the_inline_ones_it_keeps() {
for name in BasicHtml::TAGS {
let tag = Tag::named(name.as_bytes()).unwrap();
assert_eq!(tag.is_inline(), InlineHtml::TAGS.contains(&name), "{name}");
}
}