mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-10 20:51:50 +00:00
ODT: table and row backgrounds show behind their cells (gap 19) (#5727)
* ODT gap 19: table and row backgrounds show behind their cells fo:background-color on a table or row wasn't read, so it was missing on screen and gone in LibreOffice after a save. WordCraft's tables have their backgrounds on their cells, so it goes on the cells with none of their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ODT: a column's default cell style shows, and transparent clears a background (gap 19) A cell with no style took the row's or table's colour over its column's default cell style, and `transparent` on a table or row fell through to its parent style's. From CodeRabbit on this PR, checked in LibreOffice 26.8.2.2: a row's default cell style isn't applied there, so it isn't here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ODT: a column's default background is read once, not its style's name (gap 19) The name was cloned for every repeat of a column, which a hostile file could make use to run out of memory, and allocation failure can't be caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ODT: a transparent cell style stops its parent's colour (gap 19) A cell style saying transparent over a coloured parent took the parent's colour and hid the row behind it. LibreOffice 26.8 shows the row, from a column's default cell style or the cell's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Gap 19: docs status lines are main's, with this change in front Merging main had kept this branch's older status lines in ROADMAP.md and docs/file-format-parity.md, which set main's 2026-11-08 date back and dropped main's newer entries and the parity doc's CLI path note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d102b2f08b
commit
d75364067d
6 files changed
+144
-7
No files matched your search
+3
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-08 · **Change:** trivial (pointer zoom stretches page rasters until it stops); previously trivial (dark-theme interface text on Linux no longer looks semi-bold); previously trivial (Control/MCP #2596 review: partial-write outcomes, path-bearing image exports, accept-thread startup cleanup and malformed UTF-8 handling); previously trivial (progress log: a turned inline picture whose file keeps no room for the turn keeps its line to its frame); previously trivial (progress log: the Arabic interface's Styles gallery reads from the right (#5566)); previously trivial (progress log: ODT pictures offered in several formats); previously trivial (progress log: ODT pictures offered in several formats); previously trivial (progress log: ODT pictures offered in several formats); previously trivial (progress log: ODT heading text of another size); previously trivial (Protected typing boundary lookup counts preceding objects once and indexes editable permission IDs (#3614); caret movement and enforcement policy are unchanged; previously trivial: progress log: Custom document properties dialog, #2982; previously trivial: progress log: View › New Window; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with Line truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-08 · **Change:** trivial (progress log: ODT table and row backgrounds); previously trivial (pointer zoom stretches page rasters until it stops); previously trivial (dark-theme interface text on Linux no longer looks semi-bold); previously trivial (Control/MCP #2596 review: partial-write outcomes, path-bearing image exports, accept-thread startup cleanup and malformed UTF-8 handling); previously trivial (progress log: a turned inline picture whose file keeps no room for the turn keeps its line to its frame); previously trivial (progress log: the Arabic interface's Styles gallery reads from the right (#5566)); previously trivial (progress log: ODT pictures offered in several formats); previously trivial (progress log: ODT pictures offered in several formats); previously trivial (progress log: ODT pictures offered in several formats); previously trivial (progress log: ODT heading text of another size); previously trivial (Protected typing boundary lookup counts preceding objects once and indexes editable permission IDs (#3614); caret movement and enforcement policy are unchanged; previously trivial: progress log: Custom document properties dialog, #2982; previously trivial: progress log: View › New Window; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously triLine truncated
|
||||
>
|
||||
> **Recipient CSV update:** 2026-10-27 · Complete quoted header records choose the delimiter (#4249); detection and regressions follow the current mailings module split. Recipient limits and readiness estimates are unchanged.
|
||||
>
|
||||
@@ -150,6 +150,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
|
||||
## Progress log
|
||||
|
||||
- 2026-11-05: ODT table and row backgrounds are kept (gap 19): they were dropped, and gone in LibreOffice after a save.
|
||||
- 2026-11-08: Layout: a turned inline picture whose file keeps no room for the turn (no or a zero `wp:effectExtent`, as other tools write) keeps its line to its frame and overhangs it, cut at the line's top and bottom, as Word does: the frame as it is in Word 2007 layout, with width and height exchanged from 45° to 135° and 225° to 315° in Word 2013+ layout. Such a file saves the same way. Pictures with the room recorded (Word's own files) still take their rotated bounds. Readiness unchanged.
|
||||
- 2026-11-08: XML text and CDATA read with their line ends normalised (XML 1.0 §2.11): CR LF and a lone CR are one line feed, ` ` stays a CR, so a line break inside `w:t` in a file saved with Windows line ends is one space, not two (#5897); readiness estimates unchanged.
|
||||
- 2026-11-08: Layout: contextual spacing drops the space after a table cell's last paragraph when that paragraph is in the Normal style, the end-of-cell mark counting as a Normal paragraph next to it; a last paragraph in another style keeps its space after. The rule is inferred from docxide-pdf's notes and its `cell-end-*` fixtures (`cell-end-text-context`, `cell-end-empty-context`); a Word comparison that tells it apart from the neighbouring paragraph's style is still open. Readiness unchanged.
|
||||
@@ -924,6 +925,7 @@ MATH glyphs that reference the same construction now share its immutable variant
|
||||
| 2026-11-05 | trivial | DOCX paragraph `w:adjustRightInd` is preserved with true, false and absent values; OOXML properties follow schema order. Grid layout remains open. |
|
||||
| 2026-11-04 | trivial | Progress log: RTF Unicode fallback counts; readiness estimates unchanged |
|
||||
| 2026-10-28 | trivial | Per-page ink object lookups, #3614. Drawing order, geometry and saved data retain their behavior; no readiness estimate change |
|
||||
| 2026-11-05 | trivial | Progress log: ODT table and row backgrounds (gap 19) |
|
||||
| 2026-11-05 | trivial | Progress log: ODT heading text of another size (gap 19) |
|
||||
| 2026-11-05 | trivial | Progress log: ODT table rows that don't break (gap 19) |
|
||||
| 2026-11-05 | trivial | Progress log: a watermark or odd and even headers change to a file is saved |
|
||||
|
||||
@@ -1300,10 +1300,12 @@ impl Body<'_> {
|
||||
}
|
||||
"table-column" => {
|
||||
let w = get(a, "table:style-name").and_then(|n| self.styles.column_width(n)).unwrap_or(0.0);
|
||||
let cells = get(a, "table:default-cell-style-name").and_then(|s| self.styles.cell_background(s));
|
||||
let n = get(a, "table:number-columns-repeated").and_then(|v| v.trim().parse::<usize>().ok()).unwrap_or(1).max(1);
|
||||
if let Some(t) = self.tables.last_mut() {
|
||||
let room = wordcraft_doc::table::MAX_COLS.saturating_sub(t.widths.len());
|
||||
t.widths.extend(std::iter::repeat_n(w, n.min(room)));
|
||||
t.column_backgrounds.extend(std::iter::repeat_n(cells, n.min(room)));
|
||||
}
|
||||
}
|
||||
"table-row" => self.start_row(a),
|
||||
@@ -1311,7 +1313,16 @@ impl Body<'_> {
|
||||
self.end_para();
|
||||
let span = get(a, "table:number-columns-spanned").and_then(|v| v.trim().parse::<u32>().ok()).unwrap_or(1).clamp(1, 63);
|
||||
let rows = get(a, "table:number-rows-spanned").and_then(|v| v.trim().parse::<u32>().ok()).unwrap_or(1).clamp(1, 1000);
|
||||
let shading = get(a, "table:style-name").and_then(|s| self.styles.cell_background(s));
|
||||
// WordCraft's tables have their backgrounds on their cells: a row's or table's on
|
||||
// those with none of their own, as it shows behind them. A cell with no style has
|
||||
// its column's default cell style; LibreOffice doesn't apply a row's.
|
||||
let behind = self.tables.last().and_then(|t| t.row_background.or(t.background));
|
||||
let in_column = self.tables.last().and_then(|t| {
|
||||
let at: usize = t.row.iter().map(|c| c.colspan.max(1) as usize).sum();
|
||||
t.column_backgrounds.get(at).copied().flatten()
|
||||
});
|
||||
let own = get(a, "table:style-name").map_or(in_column, |s| self.styles.cell_background(s));
|
||||
let shading = own.or(behind);
|
||||
let valign = get(a, "table:style-name").and_then(|s| self.styles.cell_valign(s));
|
||||
let borders = get(a, "table:style-name").and_then(|s| self.styles.cell_borders(s));
|
||||
let margins = get(a, "table:style-name").and_then(|s| self.styles.cell_padding(s));
|
||||
@@ -1393,14 +1404,26 @@ impl Body<'_> {
|
||||
let space = get(a, "table:style-name").map(|s| self.styles.table_space(s)).unwrap_or_default();
|
||||
let extra =
|
||||
model::TableExtra { align, width: None, width_pct, heights: Vec::new(), keep_rows: Vec::new(), fixed: true, space, header_rows: 0 };
|
||||
self.tables.push(TableB { rows: Vec::new(), row: Vec::new(), hcover: 0, widths: Vec::new(), extra });
|
||||
let background = get(a, "table:style-name").and_then(|s| self.styles.table_background(s));
|
||||
self.tables.push(TableB {
|
||||
rows: Vec::new(),
|
||||
row: Vec::new(),
|
||||
hcover: 0,
|
||||
widths: Vec::new(),
|
||||
column_backgrounds: Vec::new(),
|
||||
extra,
|
||||
background,
|
||||
row_background: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// A table row (`table:table-row`) begins: its height, kept as rows are (see the row's end).
|
||||
fn start_row(&mut self, a: &Attrs) {
|
||||
let height = get(a, "table:style-name").and_then(|s| self.styles.row_height(s));
|
||||
let background = get(a, "table:style-name").and_then(|s| self.styles.table_background(s));
|
||||
let keep = self.styles.row_keep(get(a, "table:style-name"));
|
||||
if let Some(t) = self.tables.last_mut() {
|
||||
t.row_background = background;
|
||||
t.row.clear();
|
||||
t.hcover = 0;
|
||||
if t.extra.heights.len() < wordcraft_doc::table::MAX_ROWS {
|
||||
|
||||
@@ -75,6 +75,11 @@ pub(in crate::odt) struct TableB {
|
||||
pub(in crate::odt) widths: Vec<f32>,
|
||||
/// Its alignment and width, and each row's height so far (see [`crate::model::TableExtra`]).
|
||||
pub(in crate::odt) extra: crate::model::TableExtra,
|
||||
/// The background of each column's default cell style (`table:default-cell-style-name`).
|
||||
pub(in crate::odt) column_backgrounds: Vec<Option<wordcraft_doc::props::Rgb>>,
|
||||
/// The table's background, and the row's being read.
|
||||
pub(in crate::odt) background: Option<wordcraft_doc::props::Rgb>,
|
||||
pub(in crate::odt) row_background: Option<wordcraft_doc::props::Rgb>,
|
||||
}
|
||||
|
||||
impl TableB {
|
||||
|
||||
@@ -229,7 +229,7 @@ struct StyleInfo {
|
||||
outline: Option<u8>,
|
||||
border_bottom: bool,
|
||||
/// `fo:background-color` of a table-cell style's `style:table-cell-properties`.
|
||||
cell_background: Option<Rgb>,
|
||||
cell_background: Option<Option<Rgb>>,
|
||||
/// A table-cell style's background picture (§17.3 `style:background-image`): its package
|
||||
/// path, `style:repeat` and `style:position`.
|
||||
cell_picture: Option<(String, String, String)>,
|
||||
@@ -252,6 +252,9 @@ struct StyleInfo {
|
||||
/// A table-row style's `style:table-row-properties`: its height (`style:row-height`, exact)
|
||||
/// or least height (`style:min-row-height`), points.
|
||||
row_height: Option<(f32, bool)>,
|
||||
/// `fo:background-color` of a table or table-row style (§20.175): what shows behind cells
|
||||
/// with none of their own. `Some(None)`: `transparent`, which clears a parent style's.
|
||||
table_background: Option<Option<Rgb>>,
|
||||
/// `fo:keep-together` of a table-row style: the row doesn't break across pages.
|
||||
row_keep: Option<bool>,
|
||||
/// `style:master-page-name` of a paragraph or table style: its paragraphs (tables) start a
|
||||
@@ -573,6 +576,12 @@ pub(super) fn parse_border(v: &str) -> Option<Border> {
|
||||
|
||||
/// A property element `name` (`q` in full) of the style being read, into `s`: true when it is
|
||||
/// one (paragraph, table, column, cell, section and graphic properties, tab stops).
|
||||
/// What `fo:background-color` says of a table or row: a colour, or none (`transparent`).
|
||||
fn table_background_of(a: &Attrs) -> Option<Option<Rgb>> {
|
||||
let bg = get(a, "fo:background-color")?;
|
||||
if bg.trim() == "transparent" { Some(None) } else { model::parse_color(bg).map(Some) }
|
||||
}
|
||||
|
||||
fn style_child(name: &str, q: &str, a: &Attrs, s: &mut StyleInfo) -> bool {
|
||||
match name {
|
||||
"paragraph-properties" => paragraph_properties(a, s),
|
||||
@@ -599,6 +608,7 @@ fn style_child(name: &str, q: &str, a: &Attrs, s: &mut StyleInfo) -> bool {
|
||||
s.page_number = Some(v.trim().parse::<u32>().ok().map(|n| n.min(32_767)));
|
||||
}
|
||||
// §20.300 `table:align`: `margins` fills the text width, as no alignment does.
|
||||
s.table_background = table_background_of(a).or(s.table_background);
|
||||
s.table_align = match get(a, "table:align").map(str::trim) {
|
||||
Some("left") => Some(Align::Left),
|
||||
Some("center") => Some(Align::Center),
|
||||
@@ -620,6 +630,7 @@ fn style_child(name: &str, q: &str, a: &Attrs, s: &mut StyleInfo) -> bool {
|
||||
}
|
||||
}
|
||||
"table-row-properties" => {
|
||||
s.table_background = table_background_of(a).or(s.table_background);
|
||||
if let Some(h) = get(a, "style:row-height").and_then(length).filter(|h| h.is_finite() && *h > 0.0) {
|
||||
s.row_height = Some((h.min(MAX_LEN), true));
|
||||
} else if let Some(h) = get(a, "style:min-row-height").and_then(length).filter(|h| h.is_finite() && *h > 0.0) {
|
||||
@@ -677,7 +688,7 @@ pub(super) fn writing_direction(m: &str) -> wordcraft_doc::props::TextDirection
|
||||
/// A table-cell style's `style:table-cell-properties` (ODF 1.3 §17.18): background, borders,
|
||||
/// padding and vertical alignment.
|
||||
fn table_cell_properties(a: &Attrs, s: &mut StyleInfo) {
|
||||
if let Some(c) = get(a, "fo:background-color").and_then(model::parse_color) {
|
||||
if let Some(c) = table_background_of(a) {
|
||||
s.cell_background = Some(c);
|
||||
}
|
||||
cell_borders_and_padding(a, s);
|
||||
@@ -1379,8 +1390,13 @@ impl Styles {
|
||||
self.chain(name).iter().find_map(|(_, s)| s.cell_picture.clone())
|
||||
}
|
||||
|
||||
pub(super) fn table_background(&self, name: &str) -> Option<Rgb> {
|
||||
self.chain(name).iter().find_map(|(_, s)| s.table_background).flatten()
|
||||
}
|
||||
|
||||
/// Background of a table-cell style (whole chain).
|
||||
pub(super) fn cell_background(&self, name: &str) -> Option<Rgb> {
|
||||
self.chain(name).iter().find_map(|(_, s)| s.cell_background)
|
||||
self.chain(name).iter().find_map(|(_, s)| s.cell_background).flatten()
|
||||
}
|
||||
|
||||
/// Borders of a table-cell style (whole chain).
|
||||
|
||||
@@ -160,6 +160,96 @@ fn a_table_starting_a_page_starts_it() {
|
||||
assert_eq!(c.matches(r#"fo:break-before="page""#).count(), 2, "{c}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tables_and_rows_background_show_behind_their_cells() {
|
||||
// Table Properties › Background: it was dropped, on screen and in LibreOffice after a save.
|
||||
let styles = r##"<style:style style:name="T" style:family="table"><style:table-properties fo:background-color="#ddffdd"/></style:style><style:style style:name="R" style:family="table-row"><style:table-row-properties fo:background-color="#ffdddd"/></style:style><style:style style:name="C" style:family="table-cell"><style:table-cell-properties fo:background-color="#ddddff"/></style:style>"##;
|
||||
let table = r#"<table:table table:style-name="T"><table:table-row><table:table-cell><text:p>table</text:p></table:table-cell><table:table-cell table:style-name="C"><text:p>cell</text:p></table:table-cell></table:table-row><table:table-row table:style-name="R"><table:table-cell><text:p>row</text:p></table:table-cell><table:table-cell table:style-name="C"><text:p>cell</text:p></table:table-cell></table:table-row></table:table>"#;
|
||||
let colours = |d: &wordcraft_doc::Document| {
|
||||
d.body
|
||||
.iter()
|
||||
.find_map(|b| b.as_table())
|
||||
.map(|t| t.rows.iter().flat_map(|r| r.cells.iter().map(|c| c.props.shading.map(|s| s.hex().to_ascii_lowercase()))).collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let want = |t: &str, r: &str, c: &str| vec![Some(t.to_string()), Some(c.to_string()), Some(r.to_string()), Some(c.to_string())];
|
||||
let d = doc(styles, table);
|
||||
assert_eq!(colours(&d), want("ddffdd", "ffdddd", "ddddff"));
|
||||
assert_eq!(colours(&import(&crate::odt::tests::export_model(&d).unwrap()).unwrap()), want("ddffdd", "ffdddd", "ddddff"));
|
||||
}
|
||||
|
||||
/// As LibreOffice shows them: a cell's own style, else its column's default cell style (a row's
|
||||
/// isn't applied), else the row's background, else the table's. A row or table clearing its
|
||||
/// background (`transparent`) doesn't take its parent's; the table's shows behind that row.
|
||||
#[test]
|
||||
fn a_cells_background_comes_from_its_column_then_row_then_table() {
|
||||
let cell = |n: &str, c: &str| {
|
||||
format!(
|
||||
r##"<style:style style:name="{n}" style:family="table-cell"><style:table-cell-properties fo:background-color="#{c}"/></style:style>"##
|
||||
)
|
||||
};
|
||||
let styles = format!(
|
||||
r##"{}{}<style:style style:name="T" style:family="table"><style:table-properties fo:background-color="#ffff00"/></style:style><style:style style:name="R" style:family="table-row"><style:table-row-properties fo:background-color="#0000ff"/></style:style><style:style style:name="Clear" style:family="table-row" style:parent-style-name="R"><style:table-row-properties fo:background-color="transparent"/></style:style>"##,
|
||||
cell("InRow", "ff0000"),
|
||||
cell("InColumn", "00ff00"),
|
||||
);
|
||||
let c = "<table:table-cell><text:p>x</text:p></table:table-cell>";
|
||||
let table = format!(
|
||||
r#"<table:table table:style-name="T"><table:table-column table:default-cell-style-name="InColumn"/><table:table-column/><table:table-row table:style-name="R" table:default-cell-style-name="InRow">{c}{c}</table:table-row><table:table-row>{c}{c}</table:table-row><table:table-row table:style-name="Clear">{c}{c}</table:table-row></table:table>"#
|
||||
);
|
||||
let colours = |d: &wordcraft_doc::Document| {
|
||||
d.body
|
||||
.iter()
|
||||
.find_map(|b| b.as_table())
|
||||
.map(|t| t.rows.iter().flat_map(|r| r.cells.iter().map(|c| c.props.shading.map(|s| s.hex().to_ascii_lowercase()))).collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let s = |v: &str| Some(v.to_string());
|
||||
let want = vec![s("00ff00"), s("0000ff"), s("00ff00"), s("ffff00"), s("00ff00"), s("ffff00")];
|
||||
let d = doc(&styles, &table);
|
||||
assert_eq!(colours(&d), want);
|
||||
assert_eq!(colours(&import(&crate::odt::tests::export_model(&d).unwrap()).unwrap()), want);
|
||||
}
|
||||
|
||||
/// A column default after a spanning cell belongs to the column the cell is in.
|
||||
#[test]
|
||||
fn a_column_default_counts_spanned_columns() {
|
||||
let styles =
|
||||
r##"<style:style style:name="C" style:family="table-cell"><style:table-cell-properties fo:background-color="#00ff00"/></style:style>"##;
|
||||
let table = r#"<table:table><table:table-column/><table:table-column/><table:table-column table:default-cell-style-name="C"/><table:table-row><table:table-cell table:number-columns-spanned="2"><text:p>a</text:p></table:table-cell><table:covered-table-cell/><table:table-cell><text:p>b</text:p></table:table-cell></table:table-row></table:table>"#;
|
||||
let d = doc(styles, table);
|
||||
let t = d.body.iter().find_map(|b| b.as_table()).unwrap();
|
||||
let last = t.rows[0].cells.iter().rev().find(|c| !c.blocks.is_empty()).unwrap();
|
||||
assert_eq!(last.props.shading.map(|s| s.hex().to_ascii_lowercase()).as_deref(), Some("00ff00"));
|
||||
}
|
||||
|
||||
/// A column's default cell style that names nothing there is no background, however long its name.
|
||||
#[test]
|
||||
fn a_column_default_naming_no_style_is_no_background() {
|
||||
let long = "x".repeat(100_000);
|
||||
let table = format!(
|
||||
r#"<table:table><table:table-column table:number-columns-repeated="63" table:default-cell-style-name="{long}"/><table:table-row><table:table-cell><text:p>a</text:p></table:table-cell></table:table-row></table:table>"#
|
||||
);
|
||||
let d = doc("", &table);
|
||||
let t = d.body.iter().find_map(|b| b.as_table()).unwrap();
|
||||
assert_eq!(t.rows[0].cells[0].props.shading, None);
|
||||
}
|
||||
|
||||
/// A cell style saying `transparent` over a coloured parent has no background of its own: what's
|
||||
/// behind it shows, in LibreOffice too, from a column's default cell style or the cell's own.
|
||||
#[test]
|
||||
fn a_transparent_cell_style_stops_its_parents_colour() {
|
||||
let styles = r##"<style:style style:name="PC" style:family="table-cell"><style:table-cell-properties fo:background-color="#ff0000"/></style:style><style:style style:name="KC" style:family="table-cell" style:parent-style-name="PC"><style:table-cell-properties fo:background-color="transparent"/></style:style><style:style style:name="R" style:family="table-row"><style:table-row-properties fo:background-color="#0000ff"/></style:style>"##;
|
||||
for (column, cell) in [(r#" table:default-cell-style-name="KC""#, ""), ("", r#" table:style-name="KC""#)] {
|
||||
let table = format!(
|
||||
r#"<table:table><table:table-column{column}/><table:table-row table:style-name="R"><table:table-cell{cell}><text:p>a</text:p></table:table-cell></table:table-row></table:table>"#
|
||||
);
|
||||
let d = doc(styles, &table);
|
||||
let t = d.body.iter().find_map(|b| b.as_table()).unwrap();
|
||||
assert_eq!(t.rows[0].cells[0].props.shading, crate::model::parse_color("#0000ff"), "{column}{cell}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_that_keeps_together_says_so() {
|
||||
// Table › Text Flow: a row not to break across pages. It came back from a save free to break.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
> **2026-11-05:** DOCX paragraph `w:adjustRightInd` keeps its default and explicit values through style resolution and schema-ordered repeated saves.
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-08 · **Change:** trivial (ODT row: how tab stops are saved, corrected); previously trivial (RTF footnotes, endnotes and comments, #1053); previously trivial (XML line ends in text normalised, #5897); previously trivial (ODT pictures offered in several formats); previously trivial (ODT pictures offered in several formats); previously trivial (ODT pictures offered in several formats); previously trivial (ODT heading text of another size); previously trivial (built-in citation source lookups, #4888; current-main progress records retained); previously trivial (ODT: no settings for what the standard leaves open); previously trivial (picture brightness, contrast and transparency); previously trivial (ODT paragraph groups kept as read); previously trivial (ODT paragraphs with charts kept as read); previously trivial (ODT sections in cells and at empty paragraphs); previously trivial (ODT element audit); previously trivial (ODT reference marks written as such); previously trivial (ODT hidden sections stay hidden; lists kept as read); previously trivial (ODT manifest lists every file); previously trivial (ODT paragraphs with bookmarks, notes, objects and page-anchored frames written as read); previously trivial (ODT paragraphs with text boxes written as read); previously trivial (ODT paragraphs with comments and tracked changes written as read); previously trivial (ODT paragraphs with pictures and shapes written as read); previously trivial (ODT sections inside text boxes); previously trivial (ODT definitions sharing a name, sections around tables, chart replacement pictures, note citations, stacking order); previously trivial (ODT form definitions and indexes written as read); previously trivial (ODT gradient angles by the file's version; deviation documented); previously minor (ODT: unedited tables, pages, sections, properties and declarations written as read; no-edit loss gate); previously minor (ODT: unedited paragraphs and style definitions written as read); previously trivial (ODT author fields said not fixed); previously trivial (ODT default text colour; covered cells' content); previously trivial (ODT widow control profile case; tabs from the indent kept); previously trivial (ODT span kerning; note reference formatting; notes starting with a tab); previously trivial (ODT fixed and sender fields); previously trivial (ODT: table styles saved into their cells; ODT tables carry no Word table style; section column balance); previously trivial (ODT interpretation profile: the standard is law, what it leaves open (including unstated page margins) follows WordCraft, LibreOffice or custom settings); previously trivial (ODT form controls keep their place on the line and text areas their lines; custom shapes keep their geometry); previously trivial (ODT in-line frames placed against the text; in-line pictures saved standing on the baseline); previously trivial (ODT charts written back as they were while unchanged; frames' drawn outline; default header height); previously trivial (ODT header rows only where they repeat); previously trivial (ODT padding without a border; unequal columns; column breaks); previously trivial (ODT lines of only pictures laid out as LibreOffice does; default page without a layout); previously trivial (ODT pictures alone in a paragraph keep its size; in-line frames raised under an inherited paragraph relation); previously trivial (ODT tables fit the margins and keep their space above and below; header spacing kept); previously trivial (ODT frames anchored and placed by their style); previously trivial (ODT default font size saved; page-anchored frames placed from the page; text box paragraph styles kept; empty paragraphs keep their size); previously trivial (ODT default font size; locale-ordered dates); previously trivial (ODT page background pictures); previously trivial (ODT tables fixed layout; tables starting a page); previously trivial (emphasis marks: model, layout, DOCX `w:em`, ODT `style:text-emphasize`, `format.emphasisMark`); previously trivial (ODT variables as SET/REF fields; character styles keep what they turn off); previously trivial (ODT styles keep what they turn off; WMF/EMF pictures kept; pictures in text boxes shown by LibreOffice); previously trivial (ODT unstated page margins are LibreOffice's 2 cm; drawings outside a paragraph join the next one); previously trivial (ODT placeholders as content controls, template dates, user name fields, unsized pages, style-only templates, found with LibreOffice's own templates); previously trivial (ODT hidden form controls kept; `.ott` templates opened and saved); previously trivial (ODT fidelity re-estimated at ~90%; a kept form control made another kind is saved as that kind); previously trivial (ODT form control sizes and buttons kept); previously trivial (ODT turned text in cells and text boxes); previously trivial (ODT macros and unknown package parts kept); previously Line truncated
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-08 · **Change:** trivial (ODT table and row backgrounds); previously trivial (ODT row: how tab stops are saved, corrected); previously trivial (RTF footnotes, endnotes and comments, #1053); previously trivial (XML line ends in text normalised, #5897); previously trivial (ODT pictures offered in several formats); previously trivial (ODT pictures offered in several formats); previously trivial (ODT pictures offered in several formats); previously trivial (ODT heading text of another size); previously trivial (built-in citation source lookups, #4888; current-main progress records retained); previously trivial (ODT: no settings for what the standard leaves open); previously trivial (picture brightness, contrast and transparency); previously trivial (ODT paragraph groups kept as read); previously trivial (ODT paragraphs with charts kept as read); previously trivial (ODT sections in cells and at empty paragraphs); previously trivial (ODT element audit); previously trivial (ODT reference marks written as such); previously trivial (ODT hidden sections stay hidden; lists kept as read); previously trivial (ODT manifest lists every file); previously trivial (ODT paragraphs with bookmarks, notes, objects and page-anchored frames written as read); previously trivial (ODT paragraphs with text boxes written as read); previously trivial (ODT paragraphs with comments and tracked changes written as read); previously trivial (ODT paragraphs with pictures and shapes written as read); previously trivial (ODT sections inside text boxes); previously trivial (ODT definitions sharing a name, sections around tables, chart replacement pictures, note citations, stacking order); previously trivial (ODT form definitions and indexes written as read); previously trivial (ODT gradient angles by the file's version; deviation documented); previously minor (ODT: unedited tables, pages, sections, properties and declarations written as read; no-edit loss gate); previously minor (ODT: unedited paragraphs and style definitions written as read); previously trivial (ODT author fields said not fixed); previously trivial (ODT default text colour; covered cells' content); previously trivial (ODT widow control profile case; tabs from the indent kept); previously trivial (ODT span kerning; note reference formatting; notes starting with a tab); previously trivial (ODT fixed and sender fields); previously trivial (ODT: table styles saved into their cells; ODT tables carry no Word table style; section column balance); previously trivial (ODT interpretation profile: the standard is law, what it leaves open (including unstated page margins) follows WordCraft, LibreOffice or custom settings); previously trivial (ODT form controls keep their place on the line and text areas their lines; custom shapes keep their geometry); previously trivial (ODT in-line frames placed against the text; in-line pictures saved standing on the baseline); previously trivial (ODT charts written back as they were while unchanged; frames' drawn outline; default header height); previously trivial (ODT header rows only where they repeat); previously trivial (ODT padding without a border; unequal columns; column breaks); previously trivial (ODT lines of only pictures laid out as LibreOffice does; default page without a layout); previously trivial (ODT pictures alone in a paragraph keep its size; in-line frames raised under an inherited paragraph relation); previously trivial (ODT tables fit the margins and keep their space above and below; header spacing kept); previously trivial (ODT frames anchored and placed by their style); previously trivial (ODT default font size saved; page-anchored frames placed from the page; text box paragraph styles kept; empty paragraphs keep their size); previously trivial (ODT default font size; locale-ordered dates); previously trivial (ODT page background pictures); previously trivial (ODT tables fixed layout; tables starting a page); previously trivial (emphasis marks: model, layout, DOCX `w:em`, ODT `style:text-emphasize`, `format.emphasisMark`); previously trivial (ODT variables as SET/REF fields; character styles keep what they turn off); previously trivial (ODT styles keep what they turn off; WMF/EMF pictures kept; pictures in text boxes shown by LibreOffice); previously trivial (ODT unstated page margins are LibreOffice's 2 cm; drawings outside a paragraph join the next one); previously trivial (ODT placeholders as content controls, template dates, user name fields, unsized pages, style-only templates, found with LibreOffice's own templates); previously trivial (ODT hidden form controls kept; `.ott` templates opened and saved); previously trivial (ODT fidelity re-estimated at ~90%; a kept form control made another kind is saved as that kind); previously trivial (ODT form control sizes and buttons kept); previously trivial (ODT turned text in cells and text boxes); previously trivial (ODTLine truncated
|
||||
> **CLI path update:** 2026-11-04 · Options before a non-Unicode input filename no longer make the CLI reject that filename; text option values still require Unicode; `run --file` and `run --script` retain native paths. No ranked gap or readiness estimate changes.
|
||||
|
||||
> **Legacy form update:** 2026-10-22 · trivial: field names and selected drop-down labels decode numeric XML character references once.
|
||||
@@ -335,6 +335,7 @@ For field results, the engine parser treats a backslash in quoted arguments as e
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-08 | trivial | ODT row corrected: a file whose tab stops are measured from the indent is saved measuring from it, not always from the margin (the 2026-11-03 change); the default stops are counted from the indent |
|
||||
| 2026-11-05 | trivial | ODT: a table's or row's background (`fo:background-color`) shows behind cells with none of their own. It was dropped, on screen and in LibreOffice after a save A cell with no style takes its column's default cell style, and `transparent` clears a parent style's background. A cell style saying `transparent` stops its parent's colour, as in LibreOffice. |
|
||||
| 2026-11-08 | trivial | XML text and CDATA read with their line ends normalised (XML 1.0 §2.11): CR LF and a lone CR are one line feed, ` ` stays a CR, so a line break inside `w:t` in a file saved with Windows line ends is one space, not two (#5897) |
|
||||
| 2026-11-07 | trivial | Charts: a series without its own colour takes the accent its `c:idx` picks, as Word does, not the one its place in the file picks (#5963) |
|
||||
| 2026-11-05 | trivial | ODT: text of another size in a heading, title or code paragraph keeps it. Every run's size went with the style, on screen and in LibreOffice after a save Text at the heading's size goes with the style, stated or not, and a percentage is of the heading's size. Text in a heading style that states no size keeps a size it states. |
|
||||
|
||||
Reference in new issue
Block a user