use domq::parse;
fn decode(html: &str) -> String {
parse(html)
.root()
.map(|n| n.text_content())
.unwrap_or_default()
}
#[test]
fn c1_range_is_remapped_through_windows_1252() {
assert_eq!(decode("<p>–</p>"), "\u{2013}"); assert_eq!(decode("<p>—</p>"), "\u{2014}"); assert_eq!(decode("<p>€</p>"), "\u{20ac}"); assert_eq!(decode("<p>™</p>"), "\u{2122}"); assert_eq!(decode("<p>’s</p>"), "\u{2019}s"); assert_eq!(
decode("<p>‘’ “”</p>"),
"\u{2018}\u{2019} \u{201c}\u{201d}"
);
assert_eq!(
decode("<p>…•Ÿ</p>"),
"\u{2026}\u{2022}\u{178}"
);
}
#[test]
fn unmapped_c1_slots_decode_to_themselves() {
for code in [0x81u32, 0x8D, 0x8F, 0x90, 0x9D] {
let html = format!("<p>&#{code};</p>");
assert_eq!(decode(&html), char::from_u32(code).unwrap().to_string());
}
}
#[test]
fn neighbours_of_the_c1_range_are_untouched() {
assert_eq!(decode("<p></p>"), "\u{7f}");
assert_eq!(decode("<p> </p>"), "\u{a0}");
assert_eq!(decode("<p>©</p>"), "\u{a9}");
}
#[test]
fn invalid_code_points_become_replacement_characters() {
assert_eq!(decode("<p>�</p>"), "\u{fffd}");
assert_eq!(decode("<p>�</p>"), "\u{fffd}");
assert_eq!(decode("<p>�</p>"), "\u{fffd}"); assert_eq!(decode("<p>�</p>"), "\u{fffd}");
assert_eq!(decode("<p>�</p>"), "\u{fffd}"); assert_eq!(decode("<p>�</p>"), "\u{fffd}");
assert_eq!(decode("<p>�</p>"), "\u{fffd}");
assert_eq!(decode("<p>�</p>"), "\u{fffd}");
assert_eq!(decode("<p></p>"), "\u{10ffff}");
}
#[test]
fn malformed_numeric_references_stay_verbatim() {
assert_eq!(decode("<p>&#;</p>"), "&#;");
assert_eq!(decode("<p>&#x;</p>"), "&#x;");
assert_eq!(decode("<p>&#xZZ;</p>"), "&#xZZ;");
assert_eq!(decode("<p>&#+65;</p>"), "&#+65;");
assert_eq!(decode("<p>A</p>"), "A");
}
#[test]
fn remapping_applies_in_attributes_too() {
let dom = parse("<a title='a–b'>x</a>");
assert_eq!(dom.root().unwrap().attr("title"), Some("a\u{2013}b"));
}
#[test]
fn entity_lookahead_is_bounded_with_multibyte_text() {
let unit = format!("&{}é", "a".repeat(31));
let html = format!("<p>{}</p>", unit.repeat(80_000));
let start = std::time::Instant::now();
let dom = parse(&html);
assert!(
start.elapsed().as_millis() < 1000,
"took {:?}",
start.elapsed()
);
assert_eq!(dom.root().unwrap().text_content(), unit.repeat(80_000));
}
#[test]
fn far_semicolon_is_not_part_of_a_reference() {
assert_eq!(
decode("<p>&éééééééééééééééééé;</p>"),
"&éééééééééééééééééé;"
);
assert_eq!(
decode("<p>&bogusééééééééééééééé;</p>"),
"&bogusééééééééééééééé;"
);
assert_eq!(decode("<p>&é</p>"), "&é");
}
#[test]
fn legacy_names_decode_without_their_semicolon() {
assert_eq!(
decode("<p>& © 2026 < &hellip</p>"),
"& \u{a9} 2026 < &hellip"
);
let dom = parse("<p>AT&T</p>");
assert_eq!(dom.root().unwrap().text_content(), "AT&T");
assert!(
dom.errors()
.iter()
.any(|e| e.kind == domq::ErrorKind::MissingSemicolon)
);
assert_eq!(decode("<p>&©<</p>"), "&©<");
let dom = parse("<a href='?a=1©=2&lang=en' title='©'>x</a>");
let a = dom.root().unwrap();
assert_eq!(a.attr("href"), Some("?a=1©=2&lang=en"));
assert_eq!(a.attr("title"), Some("\u{a9}"));
}
#[test]
fn numeric_references_decode_without_their_semicolon() {
assert_eq!(decode("<p>© 2026 AZOO</p>"), "\u{a9} 2026 AZOO");
}
#[cfg(not(feature = "entities-full"))]
#[test]
fn curated_table_leaves_rare_names_verbatim() {
assert_eq!(
decode("<p>fj ⪢̸</p>"),
"fj ⪢̸"
);
}
#[cfg(feature = "entities-full")]
mod full_table {
use super::*;
#[test]
fn multi_code_point_and_long_names() {
assert_eq!(
decode("<p>⪢̸</p>"),
"\u{2aa2}\u{338}"
);
assert_eq!(decode("<p>fj</p>"), "fj");
assert_eq!(decode("<p>  </p>"), "\u{205f}\u{200a}");
assert_eq!(
decode("<p>∳</p>"),
"\u{2233}"
);
assert_eq!(decode("<p>\"	
</p>"), "\\\"\t\n");
}
#[test]
fn names_are_case_sensitive() {
assert_eq!(decode("<p>& & &Amp;</p>"), "& & &Amp;");
assert_eq!(decode("<p>Áá</p>"), "\u{c1}\u{e1}");
}
#[test]
fn unknown_names_stay_verbatim_and_are_reported() {
let dom = parse("<p>&nosuchentity; &Nosuchentity;</p>");
assert_eq!(
dom.root().unwrap().text_content(),
"&nosuchentity; &Nosuchentity;"
);
assert_eq!(decode("<p>¬anentity;</p>"), "\u{ac}anentity;");
assert!(
dom.errors()
.iter()
.any(|e| e.kind == domq::ErrorKind::UnknownEntity)
);
}
#[test]
fn full_names_decode_in_attributes() {
let dom = parse("<a title='fj→'>x</a>");
assert_eq!(dom.root().unwrap().attr("title"), Some("fj\u{2192}"));
}
}