#3166: a web page opens after a leading comment and keeps its non-breaking spaces

Addresses CodeRabbit review on #3223:
- "Skip leading HTML comments before the prefix check": a page starting
  with an exporter's <!-- comment --> was read as CSV text. Complete
  leading comments are now skipped before looking for the page's start.
- "Preserve nonbreaking spaces in HTML text imports": split_whitespace
  turned New&nbsp;&nbsp;York into "New York". The decoded cell text is
  used as is (ordinary whitespace is already collapsed), so the
  non-breaking spaces stay, as Excel keeps them.

Test: a_web_page_table_opens_after_a_leading_comment_and_keeps_nonbreaking_spaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hector VillarrealandClaude Opus 5.5 committed 2026-11-03 01:36:40 -06:00
1 parent e23994b7cb
commit 48c52e14af
2 files changed
+23 -2

No files matched your search

+11 -2
View File
@@ -357,7 +357,15 @@ fn part_too_large_message(name: &str, part: &str, size: u64) -> String {
fn read_html_table(name: &str, kind: Option<FileKind>, bytes: &[u8], csv_as_text: bool) -> Option<Result<(Workbook, Vec<String>)>> {
// Decoded as a CSV is (BOM, UTF-8, else Windows-1252/1256), so a legacy page keeps its letters.
let text = gridcraft_xlsx::decode_text(bytes);
let head: String = text.trim_start_matches('\u{feff}').trim_start().chars().take(16).collect::<String>().to_ascii_lowercase();
// Comments an exporter writes before the page (`<!-- ... -->`) don't make it something else.
let mut rest = text.trim_start_matches('\u{feff}').trim_start();
while let Some(after) = rest.strip_prefix("<!--") {
match after.find("-->").and_then(|end| after.get(end + 3..)) {
Some(more) => rest = more.trim_start(),
None => break,
}
}
let head: String = rest.chars().take(16).collect::<String>().to_ascii_lowercase();
if !["<!doctype html", "<html", "<table", "<head", "<body", "<meta"].iter().any(|t| head.starts_with(t)) {
return None;
}
@@ -375,7 +383,8 @@ fn read_html_table(name: &str, kind: Option<FileKind>, bytes: &[u8], csv_as_text
// cell keeps the text it shows.
let field = match cell.num.filter(|_| !csv_as_text) {
Some(n) => gridcraft_core::number_to_text(n),
None => cell.text.split_whitespace().collect::<Vec<_>>().join(" "),
// Whitespace is already collapsed; non-breaking spaces stay, as in Excel.
None => cell.text.clone(),
};
if field.contains(['\t', '\n', '\r', '"']) { format!("\"{}\"", field.replace('"', "\"\"")) } else { field }
})
+12
View File
@@ -656,3 +656,15 @@ fn a_web_page_table_keeps_its_characters_text_and_header_formats() {
let (wb, _) = open_bytes_with("menu.xls", &page, None, true).unwrap();
assert_eq!(wb.sheet(0).unwrap().value(CellRef::parse("B2").unwrap()), gridcraft_core::Value::text("4,50"));
}
#[test]
fn a_web_page_table_opens_after_a_leading_comment_and_keeps_nonbreaking_spaces() {
// An exporter's comment before <html> still makes a web page; `&nbsp;` stays a non-breaking
// space, as Excel keeps it, while ordinary whitespace collapses.
let page = "<!-- exported by a web app -->\n<html><body><table><tr><td>New&nbsp;&nbsp;York</td><td>a \n b</td></tr></table></body></html>";
let (wb, warnings) = open_bytes("cities.xls", page.as_bytes()).unwrap();
assert!(warnings.first().is_some_and(|w| w.contains("is a web page (HTML)")), "{warnings:?}");
let sh = wb.sheet(0).unwrap();
assert_eq!(sh.value(CellRef::parse("A1").unwrap()), gridcraft_core::Value::text("New\u{a0}\u{a0}York"));
assert_eq!(sh.value(CellRef::parse("B1").unwrap()), gridcraft_core::Value::text("a b"));
}