use crate::models::models::{AnalyzeError, Result};
use tl::VDom;
pub trait Parser {
fn parse<'a>(&self, html: &'a str) -> Result<VDom<'a>>;
}
#[derive(Debug, Default)]
pub struct HtmlParser {
parser_options: tl::ParserOptions,
}
impl HtmlParser {
pub fn new() -> Self {
Self {
parser_options: tl::ParserOptions::default(),
}
}
pub fn with_options(parser_options: tl::ParserOptions) -> Self {
Self { parser_options }
}
pub fn find_elements_by_tag<'a>(&self, dom: &'a VDom, tag_name: &str) -> Vec<&'a tl::Node<'a>> {
utils::select_all(dom, tag_name)
}
pub fn find_elements_by_class<'a>(
&self,
dom: &'a VDom,
class_name: &str,
) -> Vec<&'a tl::Node<'a>> {
let selector = format!(".{}", class_name);
utils::select_all(dom, &selector)
}
pub fn find_elements_with_attribute<'a>(
&self,
dom: &'a VDom,
attr_name: &str,
) -> Vec<&'a tl::Node<'a>> {
let selector = format!("[{}]", attr_name);
utils::select_all(dom, &selector)
}
}
impl Parser for HtmlParser {
fn parse<'a>(&self, html: &'a str) -> Result<VDom<'a>> {
tl::parse(html, self.parser_options)
.map_err(|e| AnalyzeError::ParseError(format!("Failed to parse HTML: {:?}", e)))
}
}
pub fn parse_html<'a>(html: &'a str) -> Result<VDom<'a>> {
let parser = HtmlParser::new();
parser.parse(html)
}
pub mod utils {
use tl::{Node, VDom};
pub fn select_all<'a>(dom: &'a VDom, selector: &str) -> Vec<&'a Node<'a>> {
if let Some(nodes) = dom.query_selector(selector) {
let (lower, upper) = nodes.size_hint();
let capacity = upper.unwrap_or(lower.max(16)); let mut results = Vec::with_capacity(capacity);
for node_handle in nodes {
if let Some(node) = node_handle.get(dom.parser()) {
results.push(node);
}
}
results
} else {
Vec::new()
}
}
pub fn select_first<'a>(dom: &'a VDom, selector: &str) -> Option<&'a Node<'a>> {
if let Some(mut nodes) = dom.query_selector(selector) {
if let Some(node_handle) = nodes.next() {
return node_handle.get(dom.parser());
}
}
None
}
pub fn select_all_within<'a>(
node: &'a Node<'a>,
selector: &str,
dom: &'a VDom,
) -> Vec<&'a Node<'a>> {
let mut results = Vec::new();
fn recurse<'a>(
node: &'a Node<'a>,
selector: &str,
dom: &'a VDom,
results: &mut Vec<&'a Node<'a>>,
) {
if let Node::Tag(tag) = node {
if tag.name().as_utf8_str().eq_ignore_ascii_case(selector) {
results.push(node);
}
for child_id in tag.children().top().iter() {
if let Some(child) = child_id.get(dom.parser()) {
recurse(child, selector, dom, results);
}
}
}
}
recurse(node, selector, dom, &mut results);
results
}
pub fn get_text_content(node: &Node, dom: &VDom) -> String {
let mut buffer = String::new();
get_text_content_into_buffer(node, dom, &mut buffer);
buffer
}
pub fn get_text_content_length(node: &Node, dom: &VDom) -> usize {
get_text_content_length_recursive(node, dom)
}
pub fn has_text_content(node: &Node, dom: &VDom) -> bool {
let mut stack = vec![node];
while let Some(current_node) = stack.pop() {
match current_node {
Node::Raw(bytes) => {
if !bytes.as_bytes().is_empty() {
return true;
}
}
Node::Tag(tag) => {
for child_id in tag.children().top().iter() {
if let Some(child) = child_id.get(dom.parser()) {
stack.push(child);
}
}
}
Node::Comment(_) => {} }
}
false
}
fn get_text_content_length_recursive(node: &Node, dom: &VDom) -> usize {
let mut stack = vec![node];
let mut total = 0;
while let Some(current_node) = stack.pop() {
match current_node {
Node::Raw(bytes) => {
total += bytes.as_bytes().len();
}
Node::Tag(tag) => {
for child_id in tag.children().top().iter() {
if let Some(child) = child_id.get(dom.parser()) {
stack.push(child);
}
}
}
Node::Comment(_) => {} }
}
total
}
fn get_text_content_into_buffer(node: &Node, dom: &VDom, buffer: &mut String) {
let mut stack = vec![node];
while let Some(current_node) = stack.pop() {
match current_node {
Node::Raw(bytes) => {
buffer.push_str(&String::from_utf8_lossy(bytes.as_bytes()));
}
Node::Tag(tag) => {
let tag_name = tag.name().as_utf8_str().to_lowercase();
const EXCLUDED_TAGS: &[&str] = &[
"script", "style", "noscript", "iframe", "object", "embed", "svg", "canvas",
];
if EXCLUDED_TAGS.contains(&tag_name.as_str()) {
continue;
}
for child_id in tag.children().top().iter() {
if let Some(child) = child_id.get(dom.parser()) {
stack.push(child);
}
}
}
Node::Comment(_) => {} }
}
}
pub fn get_attribute(node: &Node, attr_name: &str) -> Option<String> {
match node {
Node::Tag(tag) => tag
.attributes()
.get(attr_name)?
.map(|v| v.as_utf8_str().to_string()),
_ => None,
}
}
pub fn get_attribute_str(node: &Node, attr_name: &str) -> Option<String> {
match node {
Node::Tag(tag) => tag
.attributes()
.get(attr_name)?
.as_ref()
.map(|v| v.as_utf8_str().to_string()),
_ => None,
}
}
pub fn attribute_equals(node: &Node, attr_name: &str, expected: &str) -> bool {
match node {
Node::Tag(tag) => {
if let Some(attr) = tag.attributes().get(attr_name) {
if let Some(value) = attr {
value.as_utf8_str().as_ref() == expected
} else {
false
}
} else {
false
}
}
_ => false,
}
}
pub fn has_attribute(node: &Node, attr_name: &str) -> bool {
match node {
Node::Tag(tag) => tag.attributes().get(attr_name).is_some(),
_ => false,
}
}
pub fn is_tag(node: &Node, tag_name: &str) -> bool {
match node {
Node::Tag(tag) => tag.name().as_utf8_str().eq_ignore_ascii_case(tag_name),
_ => false,
}
}
pub fn get_tag_name(node: &Node) -> Option<String> {
match node {
Node::Tag(tag) => Some(tag.name().as_utf8_str().to_string()),
_ => None,
}
}
pub fn calculate_html_size(html: &str) -> usize {
html.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_HTML: &str = r#"<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
<meta charset="utf-8">
<style>body { margin: 0; }</style>
<script>console.log('header script');</script>
</head>
<body>
<h1 class="main-title">Main Title</h1>
<div class="content" data-test="value">
<p>Paragraph content</p>
<p style="color: red;">Styled paragraph</p>
</div>
<script>console.log('body script');</script>
<style>.footer { padding: 10px; }</style>
</body>
</html>"#;
#[test]
fn test_html_parser_creation() {
let _parser = HtmlParser::new();
assert!(true);
}
#[test]
fn test_html_parser_with_options() {
let options = tl::ParserOptions::default();
let _parser = HtmlParser::with_options(options);
assert!(true);
}
#[test]
fn test_parse_valid_html() {
let parser = HtmlParser::new();
let result = parser.parse(SAMPLE_HTML);
assert!(result.is_ok());
let dom = result.unwrap();
assert!(dom.nodes().len() > 0);
}
#[test]
fn test_parse_empty_html() {
let parser = HtmlParser::new();
let result = parser.parse("");
assert!(result.is_ok());
let _dom = result.unwrap();
assert!(true); }
#[test]
fn test_parse_malformed_html() {
let parser = HtmlParser::new();
let malformed_html = "<html><body><p>unclosed paragraph<div>unclosed div</body></html>";
let result = parser.parse(malformed_html);
assert!(result.is_ok());
}
#[test]
fn test_find_elements_by_tag() {
let parser = HtmlParser::new();
let dom = parser.parse(SAMPLE_HTML).unwrap();
let p_elements = parser.find_elements_by_tag(&dom, "p");
assert_eq!(p_elements.len(), 2);
let script_elements = parser.find_elements_by_tag(&dom, "script");
assert_eq!(script_elements.len(), 2);
let nonexistent_elements = parser.find_elements_by_tag(&dom, "article");
assert_eq!(nonexistent_elements.len(), 0);
}
#[test]
fn test_find_elements_by_class() {
let parser = HtmlParser::new();
let dom = parser.parse(SAMPLE_HTML).unwrap();
let main_title_elements = parser.find_elements_by_class(&dom, "main-title");
assert_eq!(main_title_elements.len(), 1);
let content_elements = parser.find_elements_by_class(&dom, "content");
assert_eq!(content_elements.len(), 1);
let nonexistent_elements = parser.find_elements_by_class(&dom, "nonexistent");
assert_eq!(nonexistent_elements.len(), 0);
}
#[test]
fn test_find_elements_with_attribute() {
let parser = HtmlParser::new();
let dom = parser.parse(SAMPLE_HTML).unwrap();
let data_test_elements = parser.find_elements_with_attribute(&dom, "data-test");
assert_eq!(data_test_elements.len(), 1);
let style_elements = parser.find_elements_with_attribute(&dom, "style");
assert_eq!(style_elements.len(), 1);
let nonexistent_elements = parser.find_elements_with_attribute(&dom, "nonexistent");
assert_eq!(nonexistent_elements.len(), 0);
}
#[test]
fn test_parser_trait_implementation() {
let parser = HtmlParser::new();
let result: Result<VDom> = parser.parse("<html><body>Test</body></html>");
assert!(result.is_ok());
}
#[test]
fn test_parser_default_implementation() {
let parser = HtmlParser::default();
let result = parser.parse("<html><body>Default Test</body></html>");
assert!(result.is_ok());
}
}