use crate::dom::Dom;
use crate::error::Result;
use crate::options::{Options, ReaderableOptions};
use crate::readability::{Article, Readability};
use crate::readerable::is_probably_readerable_doc;
use std::sync::LazyLock;
static DEFAULT_OPTIONS: LazyLock<Options> = LazyLock::new(Options::default);
pub struct Document<'a> {
pub(crate) doc: Dom,
html: &'a str,
}
impl<'a> Document<'a> {
pub fn new(html: &'a str) -> Self {
Self {
doc: Dom::parse_document(html).expect("HTML DOM node limit exceeded"),
html,
}
}
pub fn is_probably_readerable(&self, options: Option<ReaderableOptions>) -> bool {
is_probably_readerable_doc(&self.doc, options)
}
pub fn parse(self, url: Option<&str>, options: Option<Options>) -> Result<Article> {
let options = options.as_ref().unwrap_or(&DEFAULT_OPTIONS);
Readability::from_document(self.doc, self.html, url, options).parse()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_readerable_check() {
let s = "a ".repeat(300);
assert!(Document::new(&format!("<p>{s}</p>")).is_probably_readerable(None));
}
#[test]
fn test_not_readerable() {
assert!(!Document::new("<p>Short</p>").is_probably_readerable(None));
}
#[test]
fn empty_documents_have_no_content() {
for html in [
"",
"<html><head><title>No body content</title></head></html>",
"<html><body><img src=\"article.jpg\" alt=\"Article image\"></body></html>",
] {
assert!(matches!(
Document::new(html).parse(None, None),
Err(crate::Error::NoContent)
));
}
}
#[test]
fn resolves_urls_against_the_first_base_with_href() {
let text = "This article contains enough text to be extracted. ".repeat(8);
let html = format!(
"<svg><base href=\"https://evil.example/\"></svg><base><base href=\"/assets/\"><article><p>{text}<a href=\"story.html\">Read more</a></p></article>"
);
let article = Document::new(&html)
.parse(Some("https://example.com/read/page.html"), None)
.unwrap();
assert!(
article
.content
.contains(r#"href="https://example.com/assets/story.html""#)
);
}
}