fix(core): header cells take only align; broker kind from the class token

A header cell spanning columns would move the labels over the figures,
so header cells may carry only align. The broker kind comes from the
span's single class token as the browser splits it, so spacing in the
attribute no longer drops it (Codex re-check, round 11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
hermes 2026-10-18 13:58:59 +00:00
commit d15fc4833a

View file

@ -149,14 +149,16 @@ fn cell(td: ElementRef) -> Option<(String, Option<String>)> {
Node::Comment(_) => {} Node::Comment(_) => {}
Node::Element(element) if element.name() == "span" && class.is_none() => { Node::Element(element) if element.name() == "span" && class.is_none() => {
let span = ElementRef::wrap(child)?; let span = ElementRef::wrap(child)?;
let value = span.attr("class").filter(|value| { // One class token, as the browser splits it.
value.starts_with("text-") && value.split_whitespace().count() == 1 let mut tokens = span.attr("class")?.split_whitespace();
})?; let token = tokens
.next()
.filter(|token| token.starts_with("text-") && tokens.next().is_none())?;
if !only_attrs(span, &["class"]) { if !only_attrs(span, &["class"]) {
return None; return None;
} }
span_text = Some(leaf_text(span)?); span_text = Some(leaf_text(span)?);
class = Some(value.to_string()); class = Some(token.to_string());
} }
_ => return None, _ => return None,
} }
@ -355,7 +357,12 @@ pub fn parse_broker_summary(html: &str) -> Result<IpotPage, IdxError> {
// The columns must be labelled as IPOT labels them, in its order. // The columns must be labelled as IPOT labels them, in its order.
let headers: Option<Vec<String>> = sole_child(head, "tr").and_then(|row| { let headers: Option<Vec<String>> = sole_child(head, "tr").and_then(|row| {
row.child_elements() row.child_elements()
.map(|th| (th.value().name() == "th").then(|| leaf_text(th)).flatten()) .map(|th| {
// Only `align`: a span would move the columns under the labels.
(th.value().name() == "th" && only_attrs(th, &["align"]))
.then(|| leaf_text(th))
.flatten()
})
.map(|text| text.map(|text| clean(&text))) .map(|text| text.map(|text| clean(&text)))
.collect() .collect()
}); });
@ -850,6 +857,14 @@ mod tests {
"deeply nested divs", "deeply nested divs",
"<div>".repeat(4_000) + &"</div>".repeat(4_000) + DAY, "<div>".repeat(4_000) + &"</div>".repeat(4_000) + DAY,
), ),
(
"a column label spanning columns",
DAY.replacen(
"<th align=\"right\">B.Lot</th>",
"<th align=\"right\" colspan=\"9\">B.Lot</th>",
1,
),
),
( (
"swapped column labels", "swapped column labels",
DAY.replacen(">B.Lot<", ">B.Tmp<", 1) DAY.replacen(">B.Lot<", ">B.Tmp<", 1)
@ -860,6 +875,12 @@ mod tests {
for (what, page) in hidden.into_iter().chain(more) { for (what, page) in hidden.into_iter().chain(more) {
assert!(read(&page).is_none(), "{what}"); assert!(read(&page).is_none(), "{what}");
} }
let spaced_class = DAY.replacen("class=\"text-foreign\"", "class=\" text-foreign \"", 1);
assert_eq!(
read(&spaced_class),
Some(original.clone()),
"class spacing shows the same"
);
let script_text = DAY.replacen("></script>", ">/* invalid stock code */</script>", 1); let script_text = DAY.replacen("></script>", ">/* invalid stock code */</script>", 1);
assert_eq!( assert_eq!(
read(&script_text), read(&script_text),