pub mod adaptive;
pub mod selectors;
pub mod similarity;
pub use adaptive::{AdaptiveMatch, AdaptiveOptions, DEFAULT_THRESHOLD};
pub use selectors::{ElementHandle, HandleSliceExt, SelectorKind, TextMatch};
pub use similarity::{fingerprint, score, Fingerprint};
use encoding_rs::{Encoding, UTF_8};
#[derive(Debug, thiserror::Error)]
pub enum ParserError {
#[error("streaming rewrite error: {0}")]
Rewriting(String),
#[error("encoding error: {0}")]
Encoding(String),
}
pub struct TreeHandle {
html: scraper::Html,
source: String,
encoding: &'static Encoding,
}
impl TreeHandle {
pub fn html(&self) -> &scraper::Html {
&self.html
}
pub fn root_element(&self) -> scraper::ElementRef<'_> {
self.html.root_element()
}
pub fn source(&self) -> &str {
&self.source
}
pub fn encoding(&self) -> &'static Encoding {
self.encoding
}
}
pub fn parse_tree(bytes: &[u8], charset: Option<&str>) -> TreeHandle {
let (source, encoding) = decode(bytes, charset);
let html = scraper::Html::parse_document(&source);
TreeHandle {
html,
source,
encoding,
}
}
pub fn stream_rewrite(
bytes: &[u8],
settings: lol_html::Settings<'_, '_>,
) -> Result<Vec<u8>, ParserError> {
let mut output: Vec<u8> = Vec::with_capacity(bytes.len());
{
let mut rewriter = lol_html::HtmlRewriter::new(settings, |c: &[u8]| {
output.extend_from_slice(c);
});
rewriter
.write(bytes)
.map_err(|e| ParserError::Rewriting(e.to_string()))?;
rewriter
.end()
.map_err(|e| ParserError::Rewriting(e.to_string()))?;
}
Ok(output)
}
fn decode(bytes: &[u8], charset: Option<&str>) -> (String, &'static Encoding) {
let explicit = charset.and_then(|c| Encoding::for_label(c.as_bytes()));
let sniffed = explicit
.or_else(|| sniff_meta_charset(bytes))
.or_else(|| bom_encoding(bytes))
.unwrap_or(UTF_8);
let (cow, used, _had_malformed) = sniffed.decode(bytes);
(cow.into_owned(), used)
}
fn bom_encoding(bytes: &[u8]) -> Option<&'static Encoding> {
if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
Some(encoding_rs::UTF_8)
} else if bytes.starts_with(&[0xFE, 0xFF]) {
Some(encoding_rs::UTF_16BE)
} else if bytes.starts_with(&[0xFF, 0xFE]) {
Some(encoding_rs::UTF_16LE)
} else {
None
}
}
fn sniff_meta_charset(bytes: &[u8]) -> Option<&'static Encoding> {
let head = &bytes[..bytes.len().min(1024)];
let lower: String = head
.iter()
.map(|b| b.to_ascii_lowercase() as char)
.collect();
let idx = lower.find("charset")?;
let rest = &lower[idx + "charset".len()..];
let rest = rest.trim_start_matches([' ', '=', '"', '\'']);
let end = rest
.find(|c: char| c == '"' || c == '\'' || c == ' ' || c == ';' || c == '>' || c == '/')
.unwrap_or(rest.len());
let label = &rest[..end];
if label.is_empty() {
None
} else {
Encoding::for_label(label.as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use lol_html::{element, Settings};
use scraper::Selector;
#[test]
fn parse_tree_well_formed() {
let html = b"<!doctype html><html><body><p class='x'>hi</p></body></html>";
let h = parse_tree(html, None);
let sel = Selector::parse("p.x").unwrap();
let el = h.html().select(&sel).next().unwrap();
assert_eq!(el.text().collect::<String>(), "hi");
assert_eq!(h.encoding(), UTF_8);
}
#[test]
fn parse_tree_malformed_no_panic() {
let html = b"<html><body><div><p>oops<span attr=\"x\
<![CDATA[stuff]]></body";
let h = parse_tree(html, None);
assert!(h.html().root_element().children().count() > 0);
}
#[test]
fn parse_tree_latin1_via_explicit_label() {
let bytes: &[u8] = b"<html><body><p>caf\xE9</p></body></html>";
let h = parse_tree(bytes, Some("latin1"));
let sel = Selector::parse("p").unwrap();
let el = h.html().select(&sel).next().unwrap();
assert_eq!(el.text().collect::<String>(), "café");
}
#[test]
fn parse_tree_sjis_via_meta_sniff() {
let mut bytes: Vec<u8> =
b"<html><head><meta charset=\"shift_jis\"></head><body><p>".to_vec();
bytes.extend_from_slice(&[0x93, 0xFA, 0x96, 0x7B]);
bytes.extend_from_slice(b"</p></body></html>");
let h = parse_tree(&bytes, None);
let sel = Selector::parse("p").unwrap();
let el = h.html().select(&sel).next().unwrap();
assert_eq!(el.text().collect::<String>(), "日本");
}
#[test]
fn parse_tree_utf8_bom() {
let mut bytes: Vec<u8> = vec![0xEF, 0xBB, 0xBF];
bytes.extend_from_slice(b"<p>x</p>");
let h = parse_tree(&bytes, None);
assert_eq!(h.encoding(), UTF_8);
}
#[test]
fn stream_rewrite_basic() {
let input = b"<a href=\"old\">link</a>";
let out = stream_rewrite(
input,
Settings {
element_content_handlers: vec![element!("a[href]", |el| {
el.set_attribute("href", "new").unwrap();
Ok(())
})],
..Settings::default()
},
)
.unwrap();
let s = String::from_utf8(out).unwrap();
assert!(s.contains("href=\"new\""));
assert!(!s.contains("old"));
}
#[test]
fn stream_rewrite_handler_error_propagates() {
let input = b"<x>";
let err = stream_rewrite(
input,
Settings {
element_content_handlers: vec![element!("x", |_el| { Err("boom".into()) })],
..Settings::default()
},
)
.unwrap_err();
matches!(err, ParserError::Rewriting(_));
}
}