From d15fc4833a9e9278e38617bbf81be1732eceffa5 Mon Sep 17 00:00:00 2001 From: hermes Date: Sun, 18 Oct 2026 13:58:59 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh --- crates/idx-core/src/api/ipot/parse.rs | 31 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/idx-core/src/api/ipot/parse.rs b/crates/idx-core/src/api/ipot/parse.rs index 3a21b0c..4cbc786 100644 --- a/crates/idx-core/src/api/ipot/parse.rs +++ b/crates/idx-core/src/api/ipot/parse.rs @@ -149,14 +149,16 @@ fn cell(td: ElementRef) -> Option<(String, Option)> { Node::Comment(_) => {} Node::Element(element) if element.name() == "span" && class.is_none() => { let span = ElementRef::wrap(child)?; - let value = span.attr("class").filter(|value| { - value.starts_with("text-") && value.split_whitespace().count() == 1 - })?; + // One class token, as the browser splits it. + 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"]) { return None; } span_text = Some(leaf_text(span)?); - class = Some(value.to_string()); + class = Some(token.to_string()); } _ => return None, } @@ -355,7 +357,12 @@ pub fn parse_broker_summary(html: &str) -> Result { // The columns must be labelled as IPOT labels them, in its order. let headers: Option> = sole_child(head, "tr").and_then(|row| { 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))) .collect() }); @@ -850,6 +857,14 @@ mod tests { "deeply nested divs", "
".repeat(4_000) + &"
".repeat(4_000) + DAY, ), + ( + "a column label spanning columns", + DAY.replacen( + "B.Lot", + "B.Lot", + 1, + ), + ), ( "swapped column labels", DAY.replacen(">B.Lot<", ">B.Tmp<", 1) @@ -860,6 +875,12 @@ mod tests { for (what, page) in hidden.into_iter().chain(more) { 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(">", ">/* invalid stock code */", 1); assert_eq!( read(&script_text),