mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-19 02:10:15 -05:00
* Keep the ODT paragraph 'Keep with next' option on import and export (#2813) * file-format-parity: keep with next on the ODT row --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
1 parent
4495e2be53
commit
e7cd8679ad
3 files changed
+44
-6
No files matched your search
@@ -133,6 +133,8 @@ pub struct Para {
|
||||
pub list: Option<ListInfo>,
|
||||
pub align: Option<Align>,
|
||||
pub page_break: bool,
|
||||
/// Keep the paragraph on the same page as the next one (ODT `fo:keep-with-next`).
|
||||
pub keep_next: bool,
|
||||
pub inlines: Vec<Inline>,
|
||||
pub spacing: Spacing,
|
||||
/// Borders the source puts on the paragraph itself (RTF `\brdrb`…).
|
||||
@@ -517,6 +519,9 @@ impl<'a> Builder<'a> {
|
||||
let old = para_props(&c.old, old_num);
|
||||
out.props.fmt_change = PropChange::boxed(self.revision(RevisionKind::Format, &c.mark), old);
|
||||
}
|
||||
if p.keep_next {
|
||||
out.props.keep_next = Some(true);
|
||||
}
|
||||
out.section.clone_from(&p.section);
|
||||
// Inlines are appended in order, so build the runs directly: inserting each one through
|
||||
// `Paragraph::insert_text` rescans every earlier run and is quadratic in their number.
|
||||
@@ -879,6 +884,7 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, tracked: bool) -> Vec<Para> {
|
||||
list,
|
||||
align: p.props.align,
|
||||
page_break: p.props.page_break_before.unwrap_or(false),
|
||||
keep_next: p.props.keep_next.unwrap_or(false),
|
||||
inlines: Vec::new(),
|
||||
spacing: Spacing::default(),
|
||||
borders,
|
||||
@@ -909,6 +915,7 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, tracked: bool) -> Vec<Para> {
|
||||
list: None,
|
||||
align: p.props.align,
|
||||
page_break: true,
|
||||
keep_next: p.props.keep_next.unwrap_or(false),
|
||||
inlines: Vec::new(),
|
||||
spacing: Spacing::default(),
|
||||
borders,
|
||||
|
||||
@@ -206,6 +206,10 @@ impl Writer {
|
||||
if let Some(n) = start.and_then(|(_, n)| *n) {
|
||||
props.push_str(&format!(" style:page-number=\"{n}\""));
|
||||
}
|
||||
|
||||
if p.keep_next {
|
||||
props.push_str(" fo:keep-with-next=\"always\"");
|
||||
}
|
||||
if p.kind == Kind::Rule {
|
||||
props.push_str(" fo:border-bottom=\"0.75pt solid #a0a0a0\" fo:padding-bottom=\"1pt\"");
|
||||
}
|
||||
@@ -1051,6 +1055,8 @@ struct StyleInfo {
|
||||
fmt: FmtDelta,
|
||||
align: Option<Align>,
|
||||
page_break: bool,
|
||||
/// `fo:keep-with-next`: `Some(true)` for "always", `Some(false)` for "auto".
|
||||
keep_next: Option<bool>,
|
||||
outline: Option<u8>,
|
||||
border_bottom: bool,
|
||||
/// `fo:background-color` of a table-cell style's `style:table-cell-properties`.
|
||||
@@ -1275,6 +1281,11 @@ impl Styles {
|
||||
if get(&a, "fo:break-before") == Some("page") {
|
||||
s.page_break = true;
|
||||
}
|
||||
match get(&a, "fo:keep-with-next") {
|
||||
Some("always") => s.keep_next = Some(true),
|
||||
Some("auto") => s.keep_next = Some(false),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(v) = get(&a, "style:page-number") {
|
||||
s.page_number = Some(v.trim().parse::<u32>().ok().map(|n| n.min(32_767)));
|
||||
}
|
||||
@@ -1377,12 +1388,12 @@ impl Styles {
|
||||
self.chain(name).iter().find_map(|(_, s)| s.cell_background)
|
||||
}
|
||||
|
||||
/// Paragraph kind, alignment, page break, rule and base formatting. The text formatting of
|
||||
/// Paragraph kind, alignment, page break, rule, keep-with-next and base formatting. The text formatting of
|
||||
/// every style in the chain applies (automatic and common, ODF 1.3 §16.2 inheritance), except
|
||||
/// the style that gives the paragraph its kind and that style's ancestors: their formatting
|
||||
/// is the kind's own look (a heading's size and colour), not direct formatting. `Standard`,
|
||||
/// the default paragraph style, holds document defaults and is left out too.
|
||||
fn para(&self, name: &str) -> (Kind, Option<Align>, bool, bool, Fmt) {
|
||||
fn para(&self, name: &str) -> (Kind, Option<Align>, bool, bool, bool, Fmt) {
|
||||
let chain = self.chain(name);
|
||||
let mut kind = Kind::Normal;
|
||||
let mut kind_at = chain.len();
|
||||
@@ -1417,7 +1428,9 @@ impl Styles {
|
||||
for (_, s) in chain.iter().take(kind_at).rev().filter(|(n, s)| s.auto || *n != "Standard") {
|
||||
s.fmt.apply(&mut f);
|
||||
}
|
||||
(kind, align, pb, rule, f)
|
||||
// Like the text formatting, the kind style's own `fo:keep-with-next` (a heading's) is the kind's look, not direct formatting.
|
||||
let keep_next = chain.iter().take(kind_at).find_map(|(_, s)| s.keep_next).unwrap_or(false);
|
||||
(kind, align, pb, rule, keep_next, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1576,7 +1589,8 @@ impl Body<'_> {
|
||||
|
||||
fn start_para(&mut self, style: Option<&str>, heading: Option<u8>) {
|
||||
self.end_para();
|
||||
let (mut kind, align, mut pb, rule, base) = style.map(|s| self.styles.para(s)).unwrap_or((Kind::Normal, None, false, false, Fmt::default()));
|
||||
let (mut kind, align, mut pb, rule, keep_next, base) =
|
||||
style.map(|s| self.styles.para(s)).unwrap_or((Kind::Normal, None, false, false, false, Fmt::default()));
|
||||
// The section break starts the new page; no page break besides.
|
||||
if let Some(next) = style.and_then(|s| self.page_section(s))
|
||||
&& self.break_section(next)
|
||||
@@ -1586,7 +1600,7 @@ impl Body<'_> {
|
||||
if let Some(h) = heading {
|
||||
kind = Kind::Heading(h.clamp(1, 6));
|
||||
}
|
||||
let mut p = Para { kind, align, page_break: pb, ..Default::default() };
|
||||
let mut p = Para { kind, align, page_break: pb, keep_next, ..Default::default() };
|
||||
if rule && kind == Kind::Normal {
|
||||
p.kind = Kind::Rule;
|
||||
}
|
||||
@@ -3065,6 +3079,23 @@ mod tests {
|
||||
assert_eq!(secs[0].1.columns, wordcraft_doc::section::Columns::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keep_with_next_survives_import_and_export() {
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:automatic-styles>
|
||||
<style:style style:name="Keep" style:family="paragraph"><style:paragraph-properties fo:keep-with-next="always"/></style:style>
|
||||
<style:style style:name="Loose" style:family="paragraph" style:parent-style-name="Keep"><style:paragraph-properties fo:keep-with-next="auto"/></style:style>
|
||||
</office:automatic-styles><office:body><office:text>
|
||||
<text:p text:style-name="Keep">Kept</text:p><text:p text:style-name="Loose">Loose</text:p><text:p>Plain</text:p>
|
||||
</office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let doc = import(&package(&content, None)).unwrap();
|
||||
let flags = |d: &Document| d.body.iter().filter_map(|b| b.as_para()).map(|p| p.props.keep_next).collect::<Vec<_>>();
|
||||
assert_eq!(flags(&doc), [Some(true), None, None]);
|
||||
let again = import(&export(&doc).unwrap()).unwrap();
|
||||
assert_eq!(flags(&again), [Some(true), None, None]);
|
||||
}
|
||||
|
||||
// --- Tracked changes (ODF 1.3 Part 3 §5.5) -------------------------------------------------
|
||||
|
||||
const NS_TC: &str = r#"xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/""#;
|
||||
|
||||
@@ -42,7 +42,7 @@ Weights are the share of a Word user's file traffic (estimate). Read/write: ✅
|
||||
| PDF export | 8% | W | — | ✅ | ~80% | 6–10 | `crates/pdf/src/tests.rs` | krilla: embedded subsets, selectable text incl. field results (#133); Hebrew and Arabic copy and search as whole words (right-to-left glyphs written in visual order, #1383); All Caps and Small Caps text extracts as the capitals shown, as from Word's PDFs, and never changes the case of other text (#1473), links, outline, tagging. Outline destinations follow leading page breaks to the heading text. Fonts it can't subset become outlines (#132). Print Markup (#589): comments in balloons beside each page (scaled to its paper) and tracked changes; HTML/Markdown/text copies end with a comment list. Horizontal and vertical lines inside groups are drawn (#1474). No PDF/A option, no "best for printing/online" choice |
|
||||
| PDF open (PDF Reflow, Word for Windows) | 1% | R | ❌ | — | 0% | 30–50 | — | Converting a PDF into an editable document. A sibling app (PdfCraft) may supply the parser |
|
||||
| `.rtf` Rich Text Format | 3% | R/W | 🟡 | 🟡 | ~50% | 10–15 | `crates/formats/src/tests.rs` | ~1,500 lines: text, character/paragraph formatting, styles, lists, tables, pictures (`\shppict`, #154), fields, bookmarks. 8-bit text in the document's code page (`\ansicpg`) or the current font's (`\fcharset`, `\cpg`): Cyrillic, Central European, Greek, Turkish, Hebrew, Arabic, Baltic, Vietnamese, Thai (was Windows-1252 only), Mac OS Roman (`\mac`, `\fcharset77`) and DOS code pages (`\pc` 437, `\pca` 850, `\cpg` 437/737/850/852/857/866, `\fcharset254`) (#1653). Double-byte East Asian code pages (Japanese `\fcharset128`, Chinese, Korean) not decoded yet. Pasted from the system clipboard where the platform offers it (Windows, Wayland) when there is no HTML (#687). Tracked insertions and deletions read and written (`\revtbl`, `\revised`/`\deleted`, authors, `\revdttm` dates), so Accept/Reject All work after opening or a round trip; tracked paragraph marks (`\par` inside `\revised`/`\deleted` groups, including a `\sect` inside a deletion) and character and paragraph formatting revisions (`\crauth`/`\crdate` with `\oldcprops`, `\prauth`/`\prdate` with `\oldpprops`) read and written; section and table formatting revisions (`\oldsprops`, `\oldtprops`) not yet. Sections on import (#1378): each `\sect` ends a section with its own page size and orientation, margins, header/footer distance, columns, break type, page-number restart, start and format, title page and headers/footers (those it leaves out link to the previous); export still writes one section. Thin: comments, notes, section export, RTL |
|
||||
| `.odt` OpenDocument Text | 2% | R/W | 🟡 | 🟡 | ~50% | 8–12 | `crates/formats/src/tests.rs`, `crates/formats/src/odt.rs` unit tests, `crates/engine/tests/odt_sections.rs` | Styles, lists (#87), tables, pictures, headers/footers, page breaks (#86), page size, orientation and margins (#810), sections: one per master-page change on a paragraph or table (next page, with its own page size, orientation, margins, columns and page-numbering restart) and per columned `text:section` (continuous, with its columns and separator line) (#1379); export writes them back: a master page and page layout per new-page section (size, orientation, margins, columns, page-numbering restart on the first paragraph or table), a `text:section` with its columns per continuous section whose columns differ from the page's (one column included), a list cut by a section break goes on numbering. Limits: a continuous section's page-numbering restart isn't written (ODF applies it only with a master page); a continuous section with the page's columns merges into the section before it on reopening; even and odd page starts are written as plain new pages; a tracked deletion of a section break is lost; an empty last section is dropped; tracked insertions and deletions with author and date, inserted and deleted paragraph marks and text inserted then deleted included (ODF change tracking; format changes read as none; pictures and tables inside a tracked deletion are dropped). No footnotes, comments, fields or RTL |
|
||||
| `.odt` OpenDocument Text | 2% | R/W | 🟡 | 🟡 | ~50% | 8–12 | `crates/formats/src/tests.rs`, `crates/formats/src/odt.rs` unit tests, `crates/engine/tests/odt_sections.rs` | Styles, lists (#87), tables, pictures, headers/footers, page breaks (#86), keep with next (#2813), page size, orientation and margins (#810), sections: one per master-page change on a paragraph or table (next page, with its own page size, orientation, margins, columns and page-numbering restart) and per columned `text:section` (continuous, with its columns and separator line) (#1379); export writes them back: a master page and page layout per new-page section (size, orientation, margins, columns, page-numbering restart on the first paragraph or table), a `text:section` with its columns per continuous section whose columns differ from the page's (one column included), a list cut by a section break goes on numbering. Limits: a continuous section's page-numbering restart isn't written (ODF applies it only with a master page); a continuous section with the page's columns merges into the section before it on reopening; even and odd page starts are written as plain new pages; a tracked deletion of a section break is lost; an empty last section is dropped; tracked insertions and deletions with author and date, inserted and deleted paragraph marks and text inserted then deleted included (ODF change tracking; format changes read as none; pictures and tables inside a tracked deletion are dropped). No footnotes, comments, fields or RTL |
|
||||
| `.rtf` Rich Text Format | 3% | R/W | 🟡 | 🟡 | ~50% | 10–15 | `crates/formats/src/tests.rs` | ~1,500 lines: text, character/paragraph formatting, styles, lists, tables, pictures (`\shppict`, #154), fields, bookmarks. 8-bit text in the document's code page (`\ansicpg`) or the current font's (`\fcharset`, `\cpg`): Cyrillic, Central European, Greek, Turkish, Hebrew, Arabic, Baltic, Vietnamese, Thai (was Windows-1252 only), Mac OS Roman (`\mac`, `\fcharset77`) and DOS code pages (`\pc` 437, `\pca` 850, `\cpg` 437/737/850/852/857/866, `\fcharset254`) (#1653). Double-byte East Asian code pages (Japanese `\fcharset128`, Chinese, Korean) not decoded yet. Pasted from the system clipboard where the platform offers it (Windows, Wayland) when there is no HTML (#687). Tracked insertions and deletions read and written (`\revtbl`, `\revised`/`\deleted`, authors, `\revdttm` dates), so Accept/Reject All work after opening or a round trip; paragraph-mark and formatting revisions not yet. Sections on import (#1378): each `\sect` ends a section with its own page size and orientation, margins, header/footer distance, columns, break type, page-number restart, start and format, title page and headers/footers (those it leaves out link to the previous); export writes every section too (break type, page size and orientation, margins, header/footer distance, columns, page numbering, title page; not headers/footers). Thin: comments, notes, RTL |
|
||||
| `.txt` Plain text | 2% | R/W | ✅ | ✅ | ~95% | 1–2 | unit tests | Encoding detection. Files with other extensions (`.m3u`/`.m3u8` playlists, logs, config files, no extension) open as text when they are text, Word and RTF files by their signature; binary files are refused (#569). A Word or RTF extension that contradicts the content also opens by signature, as in Word: RTF saved as `.doc` (hh.ru résumés, accounting exports), a `.docx` renamed `.doc`, a Word 97-2003 file renamed `.docx`. Word's encoding choice dialog and line-ending options missing; such files save only through Save As |
|
||||
| `.htm/.html` Web Page and Web Page, Filtered | 1% | R/W | 🟡 | 🟡 | ~60% | 6–10 | `crates/formats/src/tests.rs` | Tables (#102), relative pictures embedded (#113), missing `</head>` tolerated (#154). Header cells (`<th>`) bold unless their CSS says otherwise; `text-align` on a cell, or inherited from its row, row group or table, aligns the cell's paragraphs (#1381). HTML gives no page size, so it opens on the region's paper (A4 or Letter), as Markdown and plain text do; `@page` isn't read (#1381). No Word "Web Page" (with `_files` folder) round-trip. The same reader takes HTML from the system clipboard: Paste keeps another program's formatting (a Word document, a web page; Windows' "HTML Format" header understood), Merge Formatting keeps its emphasis (#687). A pasted `<blockquote>` (LibreOffice's paragraph indented on both sides) becomes a Normal paragraph indented by the page's `blockquote` margins, else 0.5 in each side; opening an .html file keeps it as the Quote style (#1270). Equations are written as MathML (with the linear format as a UnicodeMath annotation) and read back from MathML: our annotation, else the presentation elements, else a TeX annotation (#816). A copy of one equation alone also goes in the clipboard's MathML formats (Windows "MathML" and "MathML Presentation", macOS `public.mathml.presentation` and `public.mathml`; W3C MathML 3 §6.2.4), which Microsoft documents Word reading first when it pastes math (#1297; the paste into Word is not yet verified) |
|
||||
|
||||
Reference in new issue
Block a user