diff --git a/crates/formats/src/html.rs b/crates/formats/src/html.rs
index d651ea2462..839f973e8a 100644
--- a/crates/formats/src/html.rs
+++ b/crates/formats/src/html.rs
@@ -341,6 +341,25 @@ struct El {
struct TableB {
rows: Vec>,
row: Option>,
+ /// 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::().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 {
+ 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("").as_deref(), Some(""));
+ assert_eq!(table_style("").as_deref(), Some(""));
+ assert_eq!(table_style("").as_deref(), Some(""));
+ assert_eq!(table_style("").as_deref(), Some("TableGrid"));
+ assert_eq!(table_style("").as_deref(), Some("TableGrid"));
+ assert_eq!(table_style("").as_deref(), Some("TableGrid"));
+ }
+
#[test]
fn tag_soup() {
let f = parse(
diff --git a/crates/formats/src/model.rs b/crates/formats/src/model.rs
index 78efba93f9..f4f3ba911b 100644
--- a/crates/formats/src/model.rs
+++ b/crates/formats/src/model.rs
@@ -180,6 +180,8 @@ pub struct FTable {
pub rows: Vec>,
/// Grid column widths in points (empty = equal widths).
pub widths: Vec,
+ /// 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);
diff --git a/crates/formats/src/odt.rs b/crates/formats/src/odt.rs
index af6c134d3b..151b8c2ed4 100644
--- a/crates/formats/src/odt.rs
+++ b/crates/formats/src/odt.rs
@@ -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 {
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() {
diff --git a/crates/formats/src/rtf.rs b/crates/formats/src/rtf.rs
index ebe033ee5e..e8aebf09f9 100644
--- a/crates/formats/src/rtf.rs
+++ b/crates/formats/src/rtf.rs
@@ -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 }));
}
}