mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 17:16:14 -06:00
#3166: a web page's table keeps its characters, its shown text and its header formats
Addresses CodeRabbit review on #3223: - "Decode HTML before parsing its cells": a page in Windows-1252 read as lossy UTF-8, so "Café" lost its é. The page is now decoded as a CSV is (BOM, UTF-8, else Windows-1252/1256); decode_text is exported for it. - "Keep visible cell text when csv_as_text is enabled": opened as text, a cell with x:num took the recorded number ("4,50" became "4.5"). The recorded number is used only when values are read. - "Preserve the imported format when styling a header": a date in a <th> lost its date format when made bold and centred. Header styling now builds on the style the CSV reader gave the cell. Test: a_web_page_table_keeps_its_characters_text_and_header_formats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
44460f23cf
commit
e23994b7cb
+23
-13
@@ -355,7 +355,8 @@ fn part_too_large_message(name: &str, part: &str, size: u64) -> String {
|
||||
/// header cells (`<th>`) are bold and centred, and the sheet takes the name the page gives it
|
||||
/// (`<x:Name>`) or the file's. `None` when the bytes aren't such a page.
|
||||
fn read_html_table(name: &str, kind: Option<FileKind>, bytes: &[u8], csv_as_text: bool) -> Option<Result<(Workbook, Vec<String>)>> {
|
||||
let text = String::from_utf8_lossy(bytes);
|
||||
// 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();
|
||||
if !["<!doctype html", "<html", "<table", "<head", "<body", "<meta"].iter().any(|t| head.starts_with(t)) {
|
||||
return None;
|
||||
@@ -370,7 +371,9 @@ fn read_html_table(name: &str, kind: Option<FileKind>, bytes: &[u8], csv_as_text
|
||||
let fields: Vec<String> = row
|
||||
.iter()
|
||||
.map(|cell| {
|
||||
let field = match cell.num {
|
||||
// The number the page recorded (`x:num`) only when values are read; as text, the
|
||||
// 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(" "),
|
||||
};
|
||||
@@ -384,17 +387,24 @@ fn read_html_table(name: &str, kind: Option<FileKind>, bytes: &[u8], csv_as_text
|
||||
let read = gridcraft_xlsx::read_csv_with_report(tsv.as_bytes(), &opts).map_err(|e| EngineError::Other(e.to_string()));
|
||||
Some(read.map(|(mut wb, report)| {
|
||||
wb.csv_delimiter = None;
|
||||
let header_style = wb.styles.derive(gridcraft_model::StyleId::DEFAULT, |st| {
|
||||
st.font.bold = true;
|
||||
st.align.h = gridcraft_model::HAlign::Center;
|
||||
});
|
||||
if let Some(sh) = wb.sheet_mut(0) {
|
||||
for (r, row) in rows.iter().enumerate() {
|
||||
for (c, cell) in row.iter().enumerate() {
|
||||
if cell.header && !cell.text.trim().is_empty() {
|
||||
sh.set_style(CellRef::new(r as u32, c as u32), header_style);
|
||||
}
|
||||
}
|
||||
// Header cells are bold and centred on top of the style the CSV reader gave them, so a
|
||||
// date in a header keeps its date format.
|
||||
let headers: Vec<CellRef> = rows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(r, row)| row.iter().enumerate().filter(|(_, cell)| cell.header && !cell.text.trim().is_empty()).map(move |(c, _)| CellRef::new(r as u32, c as u32)))
|
||||
.collect();
|
||||
let mut bold: std::collections::HashMap<gridcraft_model::StyleId, gridcraft_model::StyleId> = std::collections::HashMap::new();
|
||||
for at in headers {
|
||||
let Some(base) = wb.sheet(0).map(|sh| sh.style_id(at)) else { continue };
|
||||
let id = *bold.entry(base).or_insert_with(|| {
|
||||
wb.styles.derive(base, |st| {
|
||||
st.font.bold = true;
|
||||
st.align.h = gridcraft_model::HAlign::Center;
|
||||
})
|
||||
});
|
||||
if let Some(sh) = wb.sheet_mut(0) {
|
||||
sh.set_style(at, id);
|
||||
}
|
||||
}
|
||||
let page_name = find_between(&text, "<x:Name>", "</x:Name>").map(|n| crate::clip_html::normalized(n.trim()));
|
||||
|
||||
@@ -636,3 +636,23 @@ fn a_web_page_saved_as_xls_opens_its_table() {
|
||||
let (wb, _) = open_bytes("notes.csv", b"<b>,1\nx,2\n").unwrap();
|
||||
assert_eq!(wb.sheet(0).unwrap().value(CellRef::parse("B2").unwrap()), gridcraft_core::Value::number(2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_web_page_table_keeps_its_characters_text_and_header_formats() {
|
||||
// A page saved in Windows-1252 keeps its accented letters (it read as replacement characters).
|
||||
let mut page = b"<html><body><table><tr><th>Caf".to_vec();
|
||||
page.push(0xE9);
|
||||
page.extend_from_slice(b"</th><th>2024-01-15</th></tr><tr><td>Plum</td><td x:num=\"4.5\">4,50</td></tr></table></body></html>");
|
||||
let (wb, _) = open_bytes("menu.xls", &page).unwrap();
|
||||
let sh = wb.sheet(0).unwrap();
|
||||
assert_eq!(sh.value(CellRef::parse("A1").unwrap()), gridcraft_core::Value::text("Café"));
|
||||
// A date in a header cell stays a date, bold and centred (its format was replaced).
|
||||
let b1 = wb.styles.get(sh.style_id(CellRef::parse("B1").unwrap()));
|
||||
assert!(matches!(sh.value(CellRef::parse("B1").unwrap()), gridcraft_core::Value::Number(_)));
|
||||
assert!(b1.font.bold && b1.align.h == gridcraft_model::HAlign::Center);
|
||||
assert_ne!(b1.num_fmt.as_str(), "General", "the date keeps its date format");
|
||||
assert_eq!(sh.value(CellRef::parse("B2").unwrap()), gridcraft_core::Value::number(4.5));
|
||||
// Opened as text, a cell keeps the text it shows, not the number the page recorded for it.
|
||||
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"));
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ use gridcraft_core::DateSystem;
|
||||
use gridcraft_model::Workbook;
|
||||
|
||||
pub use crypto::{DecryptError, check_encryption, decrypt};
|
||||
pub use csv::{MAX_CSV_BYTES, read_csv, read_csv_with_report, write_csv, write_csv_limited};
|
||||
pub use csv::{MAX_CSV_BYTES, decode_text, read_csv, read_csv_with_report, write_csv, write_csv_limited};
|
||||
pub use ods::read_ods;
|
||||
pub use package::MAX_PART;
|
||||
pub use passthrough::rebase_links;
|
||||
|
||||
Reference in New Issue
Block a user