HTML import: tables without borders stay borderless in .docx (#99) (#102)

This commit is contained in:
MarekSpodBiedry
2026-10-10 06:16:21 -04:00
committed by GitHub
parent f71fa219ca
commit 6f282df367
4 changed files with 59 additions and 7 deletions
+45 -2
View File
@@ -341,6 +341,25 @@ struct El {
struct TableB {
rows: Vec<Vec<Cell>>,
row: Option<Vec<Cell>>,
/// The table or one of its cells asks for borders.
bordered: bool,
}
/// Whether an element's `border` attribute or `style` declares a visible border.
fn declares_border(attrs: &[(String, String)], is_table: bool) -> bool {
if is_table && attr(attrs, "border").is_some_and(|b| b.trim().parse::<u32>().map(|n| n > 0).unwrap_or(b.trim().is_empty())) {
return true;
}
let Some(style) = attr(attrs, "style") else { return false };
style.split(';').any(|decl| {
let Some((name, value)) = decl.split_once(':') else { return false };
let name = name.trim().to_ascii_lowercase();
if !matches!(name.as_str(), "border" | "border-top" | "border-right" | "border-bottom" | "border-left") {
return false;
}
let value = value.trim().to_ascii_lowercase();
!value.is_empty() && !value.split_whitespace().any(|tok| matches!(tok, "none" | "hidden" | "0" | "0px" | "0pt"))
})
}
struct Builder {
@@ -750,7 +769,8 @@ impl Builder {
if let Some(r) = t.row.take() {
t.rows.push(r);
}
let mut ft = FTable { rows: t.rows.into_iter().filter(|r| !r.is_empty()).collect(), widths: Vec::new() };
let borderless = !t.bordered;
let mut ft = FTable { rows: t.rows.into_iter().filter(|r| !r.is_empty()).collect(), widths: Vec::new(), borderless };
if !ft.rows.is_empty() {
ft.insert_covered();
self.container().push(FBlock::Table(ft));
@@ -959,7 +979,7 @@ impl Builder {
preserve = true;
}
match kind {
ElKind::Table => self.tables.push(TableB { rows: Vec::new(), row: None }),
ElKind::Table => self.tables.push(TableB { rows: Vec::new(), row: None, bordered: declares_border(attrs, true) }),
ElKind::Tr => {
if let Some(t) = self.tables.last_mut()
&& let Some(r) = t.row.take()
@@ -969,6 +989,11 @@ impl Builder {
}
ElKind::Cell => {
let tb = self.find_open(|e| e.kind == ElKind::Tr, |e| e.kind == ElKind::Table).is_none();
if let Some(t) = self.tables.last_mut()
&& declares_border(attrs, false)
{
t.bordered = true;
}
if tb && let Some(t) = self.tables.last_mut() {
// A cell without a row: start one.
if let Some(r) = t.row.take() {
@@ -1389,6 +1414,24 @@ mod tests {
.collect()
}
fn table_style(html: &str) -> Option<String> {
let doc = crate::model::to_doc(&parse(html));
doc.body.iter().find_map(|b| match &**b {
wordcraft_doc::Block::Table(t) => Some(t.props.style.clone().unwrap_or_default()),
_ => None,
})
}
#[test]
fn borderless_tables_get_no_grid_style() {
assert_eq!(table_style("<table><tr><td>a<td>b</table>").as_deref(), Some(""));
assert_eq!(table_style("<table style='border:none'><tr><td style='border:0'>a</table>").as_deref(), Some(""));
assert_eq!(table_style("<table border=0><tr><td style='border: 0px none'>a</table>").as_deref(), Some(""));
assert_eq!(table_style("<table border=1><tr><td>a</table>").as_deref(), Some("TableGrid"));
assert_eq!(table_style("<table style='border:1px solid #000'><tr><td>a</table>").as_deref(), Some("TableGrid"));
assert_eq!(table_style("<table><tr><td style='border:1px solid #999'>a</table>").as_deref(), Some("TableGrid"));
}
#[test]
fn tag_soup() {
let f = parse(
+11 -2
View File
@@ -180,6 +180,8 @@ pub struct FTable {
pub rows: Vec<Vec<Cell>>,
/// Grid column widths in points (empty = equal widths).
pub widths: Vec<f32>,
/// The source table had no borders (HTML default); the document table gets no grid style.
pub borderless: bool,
}
impl FTable {
@@ -426,6 +428,9 @@ impl Builder<'_> {
let cols = t.cols().clamp(1, wordcraft_doc::table::MAX_COLS);
let width = self.doc.last_section.text_width().max(72.0);
let mut tb = Table::new(rows.len(), cols, width);
if t.borderless {
tb.props.style = None;
}
if t.widths.len() == cols && t.widths.iter().all(|w| w.is_finite() && *w > 1.0) {
tb.grid = t.widths.iter().map(|w| w.clamp(6.0, 1584.0)).collect();
}
@@ -704,7 +709,7 @@ fn flow_table(doc: &Document, t: &Table, depth: usize) -> FTable {
}
rows.push(cells);
}
FTable { rows, widths: t.grid.clone() }
FTable { rows, widths: t.grid.clone(), borderless: false }
}
/// The document body as a flow.
@@ -980,7 +985,11 @@ mod tests {
#[test]
fn covered_cells_inserted() {
let mut t = FTable { rows: vec![vec![Cell { rowspan: 2, ..Default::default() }, Cell::default()], vec![Cell::default()]], widths: vec![] };
let mut t = FTable {
rows: vec![vec![Cell { rowspan: 2, ..Default::default() }, Cell::default()], vec![Cell::default()]],
widths: vec![],
borderless: false,
};
t.insert_covered();
assert_eq!(t.rows[1].len(), 2);
assert!(t.rows[1][0].covered);
+2 -2
View File
@@ -1243,7 +1243,7 @@ impl Body<'_> {
if let Some(t) = self.tables.pop()
&& !t.rows.is_empty()
{
self.container().push(FBlock::Table(FTable { rows: t.rows, widths: Vec::new() }));
self.container().push(FBlock::Table(FTable { rows: t.rows, widths: Vec::new(), borderless: false }));
}
}
"desc" | "title" => self.in_desc = false,
@@ -1366,7 +1366,7 @@ pub fn parse(bytes: &[u8]) -> Result<Flow, String> {
let mut blocks = body.containers.pop().unwrap_or_default();
for t in body.tables.drain(..) {
if !t.rows.is_empty() {
blocks.push(FBlock::Table(FTable { rows: t.rows, widths: Vec::new() }));
blocks.push(FBlock::Table(FTable { rows: t.rows, widths: Vec::new(), borderless: false }));
}
}
if blocks.is_empty() {
+1 -1
View File
@@ -850,7 +850,7 @@ impl Reader {
}
if !self.table.is_empty() {
let rows = std::mem::take(&mut self.table);
self.body.push(FBlock::Table(FTable { rows, widths: Vec::new() }));
self.body.push(FBlock::Table(FTable { rows, widths: Vec::new(), borderless: false }));
}
}