use crate::converter::dom_context::TableContentSummary;
use crate::converter::utility::content::normalized_tag_name;
use std::borrow::Cow;
#[derive(Default)]
pub struct TableScan {
pub row_counts: Vec<usize>,
pub has_span: bool,
pub has_header: bool,
pub has_caption: bool,
pub nested_table_count: usize,
pub link_count: usize,
pub has_text: bool,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn scan_table(
node_handle: &tl::NodeHandle,
parser: &tl::Parser,
dom_ctx: &super::super::super::DomContext,
) -> TableScan {
let (row_counts, nested_table_count, has_span) = scan_own_structure(node_handle, parser, dom_ctx);
let content = content_summary(*node_handle, parser, dom_ctx);
TableScan {
row_counts,
has_span,
has_header: content.has_header,
has_caption: content.has_caption,
nested_table_count,
link_count: content.link_count,
has_text: content.has_text,
}
}
fn tag_name_of<'a>(
handle: &tl::NodeHandle,
tag: &'a tl::HTMLTag,
parser: &tl::Parser,
dom_ctx: &'a super::super::super::DomContext,
) -> Cow<'a, str> {
dom_ctx.tag_info(handle.get_inner(), parser).map_or_else(
|| normalized_tag_name(tag.name().as_utf8_str()).into_owned().into(),
|info| Cow::Borrowed(info.name.as_str()),
)
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn scan_own_structure(
node_handle: &tl::NodeHandle,
parser: &tl::Parser,
dom_ctx: &super::super::super::DomContext,
) -> (Vec<usize>, usize, bool) {
let mut row_counts = Vec::new();
let mut nested_table_count = 0usize;
let mut has_span = false;
let Some(tl::Node::Tag(root_tag)) = node_handle.get(parser) else {
return (row_counts, nested_table_count, has_span);
};
let mut work: Vec<tl::NodeHandle> = root_tag.children().top().iter().copied().collect();
while let Some(handle) = work.pop() {
let Some(tl::Node::Tag(tag)) = handle.get(parser) else {
continue;
};
let tag_name = tag_name_of(&handle, tag, parser, dom_ctx);
match tag_name.as_ref() {
"table" => nested_table_count += 1,
"tr" | "row" => {
let (cell_count, row_has_span) = scan_row_cells(tag, parser, dom_ctx);
row_counts.push(cell_count);
has_span |= row_has_span;
work.extend(tag.children().top().iter().copied());
}
_ => work.extend(tag.children().top().iter().copied()),
}
}
(row_counts, nested_table_count, has_span)
}
fn scan_row_cells(
row_tag: &tl::HTMLTag,
parser: &tl::Parser,
dom_ctx: &super::super::super::DomContext,
) -> (usize, bool) {
let mut cell_count = 0;
let mut has_span = false;
for child in row_tag.children().top().iter() {
let Some(tl::Node::Tag(cell_tag)) = child.get(parser) else {
continue;
};
let cell_name = tag_name_of(child, cell_tag, parser, dom_ctx);
if !matches!(cell_name.as_ref(), "td" | "th" | "cell") {
continue;
}
cell_count += super::cell::get_colspan(child, parser);
let attrs = cell_tag.attributes();
if attrs.get("colspan").is_some() || attrs.get("rowspan").is_some() {
has_span = true;
}
}
(cell_count, has_span)
}
enum ContentFrame {
Enter(tl::NodeHandle),
ExitTable(u32),
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn content_summary(
table_handle: tl::NodeHandle,
parser: &tl::Parser,
dom_ctx: &super::super::super::DomContext,
) -> TableContentSummary {
let table_id = table_handle.get_inner();
if let Some(cached) = dom_ctx.cached_table_content_summary(table_id) {
return cached;
}
let mut acc_stack: Vec<(u32, TableContentSummary)> = Vec::new();
let mut work = vec![ContentFrame::Enter(table_handle)];
while let Some(frame) = work.pop() {
match frame {
ContentFrame::Enter(handle) => visit_content_node(handle, parser, dom_ctx, &mut acc_stack, &mut work),
ContentFrame::ExitTable(id) => finish_table_accumulator(id, dom_ctx, &mut acc_stack),
}
}
dom_ctx.cached_table_content_summary(table_id).unwrap_or_default()
}
fn visit_content_node(
handle: tl::NodeHandle,
parser: &tl::Parser,
dom_ctx: &super::super::super::DomContext,
acc_stack: &mut Vec<(u32, TableContentSummary)>,
work: &mut Vec<ContentFrame>,
) {
match handle.get(parser) {
Some(tl::Node::Raw(bytes)) => {
let raw = bytes.as_utf8_str();
let decoded = crate::text::decode_html_entities_cow(raw.as_ref());
if !decoded.trim().is_empty() {
if let Some((_, acc)) = acc_stack.last_mut() {
acc.has_text = true;
}
}
}
Some(tl::Node::Tag(tag)) => {
let tag_name = tag_name_of(&handle, tag, parser, dom_ctx);
if tag_name.as_ref() == "table" {
let id = handle.get_inner();
acc_stack.push((id, TableContentSummary::default()));
work.push(ContentFrame::ExitTable(id));
} else {
apply_tag_content(&tag_name, tag, acc_stack.last_mut().map(|(_, acc)| acc));
}
work.extend(tag.children().top().iter().copied().map(ContentFrame::Enter));
}
_ => {}
}
}
fn apply_tag_content(tag_name: &str, tag: &tl::HTMLTag, acc: Option<&mut TableContentSummary>) {
let Some(acc) = acc else { return };
match tag_name {
"a" => acc.link_count += 1,
"caption" => acc.has_caption = true,
"th" => acc.has_header = true,
"img" | "graphic" if tag.attributes().get("src").is_some() || tag.attributes().get("alt").is_some() => {
acc.has_text = true;
}
"cell" => {
if let Some(Some(role)) = tag.attributes().get("role") {
if role.as_utf8_str() == "head" {
acc.has_header = true;
}
}
}
_ => {}
}
}
fn finish_table_accumulator(
id: u32,
dom_ctx: &super::super::super::DomContext,
acc_stack: &mut Vec<(u32, TableContentSummary)>,
) {
let Some((_finished_id, finished)) = acc_stack.pop() else {
return;
};
dom_ctx.cache_table_content_summary(id, finished);
if let Some((_, parent)) = acc_stack.last_mut() {
parent.merge(finished);
}
}