mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(core): read the IPOT page as a browser would around its table
Comments are removed before parsing, and an unclosed or abrupt one (<!-->, <!--->, --!>) or other <! / <? markup rejects the page; the page is read only up to its first raw-text element (it ends with one empty script). The echoed dates are read before the table and the totals after it, cell text may not hold '>', and the row loop no longer rescans trailing whitespace per row (Codex re-check, round 5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
cecfbab235
commit
1c6ec6eb28
1 changed files with 80 additions and 5 deletions
|
|
@ -201,10 +201,61 @@ fn open_tag<'a>(markup: &'a str, name: &str) -> Option<&'a str> {
|
||||||
fn plain_text(text: &str) -> Option<&str> {
|
fn plain_text(text: &str) -> Option<&str> {
|
||||||
let end = text.find('<').unwrap_or(text.len());
|
let end = text.find('<').unwrap_or(text.len());
|
||||||
let mut refs = text[..end].match_indices('&');
|
let mut refs = text[..end].match_indices('&');
|
||||||
refs.all(|(at, _)| text[at..].starts_with(" "))
|
(!text[..end].contains('>') && refs.all(|(at, _)| text[at..].starts_with(" ")))
|
||||||
.then_some(&text[end..])
|
.then_some(&text[end..])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Elements whose content is not markup; IPOT's page ends with one empty
|
||||||
|
/// script, after everything read here.
|
||||||
|
const RAW_TEXT: [&str; 11] = [
|
||||||
|
"<script",
|
||||||
|
"<style",
|
||||||
|
"<textarea",
|
||||||
|
"<template",
|
||||||
|
"<noscript",
|
||||||
|
"<title",
|
||||||
|
"<iframe",
|
||||||
|
"<xmp",
|
||||||
|
"<plaintext",
|
||||||
|
"<noembed",
|
||||||
|
"<noframes",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The page's markup as a browser reads it, up to the first raw-text
|
||||||
|
/// element: comments removed. A comment of an unusual form (`<!-->`,
|
||||||
|
/// `<!--->`, `--!>`), an unclosed one, or other `<!` / `<?` markup rejects
|
||||||
|
/// the page.
|
||||||
|
fn visible_markup(html: &str) -> Option<String> {
|
||||||
|
let mut out = String::with_capacity(html.len());
|
||||||
|
let mut rest = html;
|
||||||
|
while let Some(at) = rest.find("<!--") {
|
||||||
|
out.push_str(&rest[..at]);
|
||||||
|
let comment = &rest[at + "<!--".len()..];
|
||||||
|
let end = comment.find("-->")?;
|
||||||
|
let text = &comment[..end];
|
||||||
|
if text.starts_with('>') || text.starts_with("->") || text.contains("--!>") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
rest = &comment[end + "-->".len()..];
|
||||||
|
}
|
||||||
|
out.push_str(rest);
|
||||||
|
let lower = out.to_ascii_lowercase();
|
||||||
|
let cut = RAW_TEXT
|
||||||
|
.iter()
|
||||||
|
.filter_map(|tag| lower.find(tag))
|
||||||
|
.min()
|
||||||
|
.unwrap_or(lower.len());
|
||||||
|
let read = &lower[..cut];
|
||||||
|
let other = read
|
||||||
|
.match_indices("<!")
|
||||||
|
.any(|(at, _)| !read[at..].starts_with("<!doctype"));
|
||||||
|
if other || read.contains("<?") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
out.truncate(cut);
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
/// The `MM/DD/YYYY` value of the input with this id.
|
/// The `MM/DD/YYYY` value of the input with this id.
|
||||||
fn input_date(html: &str, id: &str) -> Option<NaiveDate> {
|
fn input_date(html: &str, id: &str) -> Option<NaiveDate> {
|
||||||
let at = html.find(&format!("id=\"{id}\""))?;
|
let at = html.find(&format!("id=\"{id}\""))?;
|
||||||
|
|
@ -222,6 +273,10 @@ fn footer<'a>(html: &'a str, label: &str) -> Option<&'a str> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_broker_summary(html: &str) -> Result<IpotPage, IdxError> {
|
pub fn parse_broker_summary(html: &str) -> Result<IpotPage, IdxError> {
|
||||||
|
let page = visible_markup(html).ok_or_else(|| {
|
||||||
|
IdxError::ParseError("ipot broker summary: unexpected markup in the page".to_string())
|
||||||
|
})?;
|
||||||
|
let html = page.as_str();
|
||||||
if html.to_ascii_lowercase().contains("invalid stock code") {
|
if html.to_ascii_lowercase().contains("invalid stock code") {
|
||||||
return Ok(IpotPage::Invalid);
|
return Ok(IpotPage::Invalid);
|
||||||
}
|
}
|
||||||
|
|
@ -242,8 +297,8 @@ pub fn parse_broker_summary(html: &str) -> Result<IpotPage, IdxError> {
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
let mut raw = BrokerSummaryRaw {
|
let mut raw = BrokerSummaryRaw {
|
||||||
from: input_date(html, "startDateBrokerSummary"),
|
from: input_date(&html[..body_start], "startDateBrokerSummary"),
|
||||||
to: input_date(html, "endDateBrokerSummary"),
|
to: input_date(&html[..body_start], "endDateBrokerSummary"),
|
||||||
..BrokerSummaryRaw::default()
|
..BrokerSummaryRaw::default()
|
||||||
};
|
};
|
||||||
let mut rounded = false;
|
let mut rounded = false;
|
||||||
|
|
@ -260,7 +315,7 @@ pub fn parse_broker_summary(html: &str) -> Result<IpotPage, IdxError> {
|
||||||
let mut at = body_start;
|
let mut at = body_start;
|
||||||
while at < body_end {
|
while at < body_end {
|
||||||
let rest = &lower[at..body_end];
|
let rest = &lower[at..body_end];
|
||||||
if rest.trim().is_empty() {
|
if rest.trim_start().is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let end = rest
|
let end = rest
|
||||||
|
|
@ -307,7 +362,7 @@ pub fn parse_broker_summary(html: &str) -> Result<IpotPage, IdxError> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut total = |label: &str| footer(html, label).and_then(&mut figure);
|
let mut total = |label: &str| footer(&html[body_end..], label).and_then(&mut figure);
|
||||||
raw.total_value = total("T. Val :");
|
raw.total_value = total("T. Val :");
|
||||||
raw.foreign_net_value = total("F. NVal :");
|
raw.foreign_net_value = total("F. NVal :");
|
||||||
raw.total_lots = total("T.Lot :");
|
raw.total_lots = total("T.Lot :");
|
||||||
|
|
@ -481,6 +536,23 @@ mod tests {
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
let reference = DAY.replacen(">828,163 <", ">828,163 <", 1);
|
let reference = DAY.replacen(">828,163 <", ">828,163 <", 1);
|
||||||
|
// A '>' a browser shows as text, the table inside a comment, and an
|
||||||
|
// abrupt `<!-->` that a browser closes at once, so rows 9 and 10 stay
|
||||||
|
// visible.
|
||||||
|
let angle = DAY.replacen(">828,163 <", ">8>28,163 <", 1);
|
||||||
|
let tbody_start = DAY.find("<tbody>").unwrap();
|
||||||
|
let tbody_end = DAY.find("</tbody>").unwrap() + "</tbody>".len();
|
||||||
|
let commented_table = DAY[..tbody_start].to_string()
|
||||||
|
+ "<!--"
|
||||||
|
+ &DAY[tbody_start..tbody_end]
|
||||||
|
+ "-->"
|
||||||
|
+ &DAY[tbody_end..];
|
||||||
|
let row8_end = rows[7] + "</tr>".len();
|
||||||
|
let abrupt = DAY[..row8_end].to_string()
|
||||||
|
+ "<!-->"
|
||||||
|
+ &DAY[row8_end..rows[9] + "</tr>".len()]
|
||||||
|
+ "-->"
|
||||||
|
+ &DAY[rows[9] + "</tr>".len()..];
|
||||||
for page in [
|
for page in [
|
||||||
short_row,
|
short_row,
|
||||||
skipped_rank,
|
skipped_rank,
|
||||||
|
|
@ -496,6 +568,9 @@ mod tests {
|
||||||
spaced_cell_end,
|
spaced_cell_end,
|
||||||
other_tag,
|
other_tag,
|
||||||
reference,
|
reference,
|
||||||
|
angle,
|
||||||
|
commented_table,
|
||||||
|
abrupt,
|
||||||
"</tbody><tbody>".to_string(),
|
"</tbody><tbody>".to_string(),
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue