use crate::DomExtractionError;
use ego_tree::NodeId;
use scraper::Html;
#[cfg(test)]
use std::{fs, io, path};
#[inline]
pub fn get_node_by_id(
node_id: NodeId,
document: &Html,
) -> Result<ego_tree::NodeRef<'_, scraper::node::Node>, DomExtractionError> {
document
.tree
.get(node_id)
.ok_or(DomExtractionError::NodeAccessError(node_id))
}
pub fn get_node_text(
node_id: NodeId,
document: &Html,
) -> Result<String, DomExtractionError> {
let mut text_fragments: Vec<String> = vec![];
let root_node = get_node_by_id(node_id, document)?;
collect_text_filtered(&root_node, &mut text_fragments);
Ok(crate::unicode::join_text_fragments(text_fragments))
}
fn collect_text_filtered(
node: &ego_tree::NodeRef<'_, scraper::node::Node>,
text_fragments: &mut Vec<String>,
) {
match node.value() {
scraper::Node::Text(txt) => {
let clean_text = txt.trim();
if !clean_text.is_empty() {
text_fragments.push(clean_text.to_string());
}
}
scraper::Node::Element(elem) => {
if !matches!(elem.name(), "script" | "noscript" | "style") {
for child in node.children() {
collect_text_filtered(&child, text_fragments);
}
}
}
_ => {
for child in node.children() {
collect_text_filtered(&child, text_fragments);
}
}
}
}
pub fn get_node_links(
node_id: NodeId,
document: &Html,
) -> Result<Vec<String>, DomExtractionError> {
let mut links: Vec<String> = vec![];
let root_node = get_node_by_id(node_id, document)?;
for node in root_node.descendants() {
if let Some(elem) = node.value().as_element()
&& let Some(link) = elem.attr("href")
{
links.push(link.trim().to_string());
};
}
Ok(links)
}
#[cfg(test)]
pub(crate) fn build_dom(html: &str) -> Html {
let document: Html = Html::parse_document(html);
document
}
#[cfg(test)]
pub(crate) fn read_file(
file_path: impl AsRef<path::Path>,
) -> Result<String, io::Error> {
let content: String = fs::read_to_string(file_path)?;
Ok(content)
}
#[cfg(test)]
pub(crate) fn build_dom_from_file(test_file_name: &str) -> Html {
let content = read_file(format!("html/{}", test_file_name)).unwrap();
build_dom(content.as_str())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tree::BODY_SELECTOR;
const TEST_1_HTML: &str = include_str!("../html/test_1.html");
const TEST_2_HTML: &str = include_str!("../html/test_2.html");
#[test]
fn test_body_selector() {
let document = build_dom(TEST_1_HTML);
let body_elements: Vec<_> = document.select(&BODY_SELECTOR).collect();
assert_eq!(body_elements.len(), 1); }
#[test]
fn test_load_file() {
let content = read_file("html/test_1.html");
assert!(content.is_ok());
assert!(!content.unwrap().is_empty());
}
#[test]
fn test_build_dom() {
let document = build_dom(TEST_2_HTML);
assert!(document.errors.len() == 1);
}
#[test]
fn test_document_always_has_body() {
let test_cases = [
"",
"<div>No body here</div>",
"<<<>>>",
"Plain text",
"<html><div>No explicit body</div></html>",
];
for html in test_cases {
let document = build_dom(html);
let body_elements: Vec<_> = document.select(&BODY_SELECTOR).collect();
assert_eq!(
body_elements.len(),
1,
"HTML parser should always provide a body tag"
);
}
}
}