mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-11 21:12:56 +00:00
Merge remote-tracking branch 'upstream/main' into m1056
# Conflicts: # docs/file-format-parity.md
This commit is contained in:
commit
ecbabc327c
5 files changed
+179
-4
No files matched your search
+3
-1
@@ -4,7 +4,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-06 · **Change:** trivial (progress log: ODT pictures offered in several formats); 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 fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial (progress log: a shape dropped just after a page break inside a paragraph stays on the new page, #4262); previously trivial (progress log: a float anchored on a later page of a long paragraph is drawn there, #4250); previously minor (progress log: `file.closeDocument` over the control channel and MCP bridge is refused while the document it names has unsaved changes; previously trivial (progress log: ODT element audit, ODT checked against 220 files from other applicatLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** 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 fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial (progress log: a shape dropped just after a page break inside a paragraph stays on the new page, #4262); previously trivial (progress log: a float anchored on a later page of a long paragraph is drawn there, #4250); previously minor (progress log: `file.closeDocument` over the control channel and MCP bridge is refused while the document it names has unsaved changes; previously trivial (progress log:Line 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.
|
||||
>
|
||||
@@ -144,6 +144,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
|
||||
## Progress log
|
||||
|
||||
- 2026-11-05: ODT heading text of another size keeps it (gap 19): a smaller word in a heading came back at the heading's size in LibreOffice after a save.
|
||||
- 2026-11-05: ODT pictures offered in several formats show and survive a save (gap 19): LibreOffice's letter templates lost theirs.
|
||||
- 2026-11-06: Windows print submissions run in separate helper processes (#5710). A helper acknowledges cancellation before it stops, so a driver stalled in a cancelled job cannot hold another job’s provider lock. A private temporary PDF and bounded settings messages cross the process boundary; normal shutdown reaps helpers and removes their inputs. The helper erases its plaintext inputs before driver work; a hard app crash during startup can still leave private temporary files. Jobs already sending still complete. Readiness estimates unchanged.
|
||||
- 2026-11-06: File › Print: before sending starts, Cancel printing stops a waiting job, frees the Print page and keeps the settings; a successfully cancelled job is never sent. Cancellation remains available after the last document closes. Quitting cancels waiting jobs and joins jobs already sending (#5710). On Windows, winprint repeats the DEVMODE conversion after sending starts. A stall during that repeat prevents cancellation and can keep shutdown waiting, even before a native job exists. Readiness estimates unchanged.
|
||||
@@ -820,6 +821,7 @@ MATH glyphs that reference the same construction now share its immutable variant
|
||||
| 2026-10-30 | trivial | Progress log: typed n-ary operators such as ∫ and ∑ wait for their limits, and any character that cannot continue a math name corrects it, as observed in Word for Windows on #4938; Enter, arrow keys and nested operators are WordCraft's own choices (#4917); readiness unchanged |
|
||||
| 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-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 heading text of another size (gap 19) |
|
||||
| 2026-11-05 | trivial | Progress log: ODT pictures offered in several formats (gap 19) |
|
||||
| 2026-10-28 | trivial | Shared MATH construction records reuse variants and assembly data without changing glyph mappings or existing limits (#3630); readiness estimates unchanged |
|
||||
| 2026-10-29 | trivial | Page drawing indexes text-box warp lookups per paragraph (#3614); requested offsets only, bounded prefix traversal, original Linux display and persistence controls; readiness estimates unchanged |
|
||||
|
||||
@@ -79,6 +79,10 @@ pub(super) struct Body<'a> {
|
||||
/// drawings were waiting before it.
|
||||
loose_open: Option<(usize, usize, usize)>,
|
||||
fmt: Vec<Fmt>,
|
||||
/// For each paragraph being read: the size its heading, title or code style gives its text.
|
||||
para_sizes: Vec<(Option<f32>, bool)>,
|
||||
/// That size for the paragraph being finished.
|
||||
para_size: Option<f32>,
|
||||
para: Option<Para>,
|
||||
/// The text read last ended in a space collapsed from XML whitespace (not a `text:s`).
|
||||
xml_space_end: bool,
|
||||
@@ -240,6 +244,8 @@ impl<'a> Body<'a> {
|
||||
xml_space_end: false,
|
||||
spaces_left: MAX_EXTRA_SPACES,
|
||||
para_cont: false,
|
||||
para_sizes: Vec::new(),
|
||||
para_size: None,
|
||||
lists: Vec::new(),
|
||||
lists_saved: Vec::new(),
|
||||
item_used: Vec::new(),
|
||||
@@ -956,6 +962,8 @@ impl Body<'_> {
|
||||
} else {
|
||||
self.para_cont = false;
|
||||
}
|
||||
let kind_size = matches!(kind, Kind::Code | Kind::Heading(_) | Kind::Title).then(|| style.and_then(|s| self.styles.named_size(s))).flatten();
|
||||
self.para_sizes.push((kind_size, style.is_some_and(|s| self.styles.states_size(s))));
|
||||
self.fmt.push(base);
|
||||
// Drawings waiting outside a paragraph come first in it, when it is the text's own (not
|
||||
// a text box's, a note's or a cell's).
|
||||
@@ -974,6 +982,7 @@ impl Body<'_> {
|
||||
self.index_marks.clear();
|
||||
let Some(mut p) = self.para.take() else { return };
|
||||
self.glue_connectors(&mut p);
|
||||
self.para_size = self.para_sizes.pop().and_then(|(size, stated)| size.filter(|_| stated));
|
||||
// A paragraph without text has its own text formatting as its mark's: its size is its
|
||||
// height, and its drawings' runs have it.
|
||||
if let Some(base) = self.fmt.pop()
|
||||
@@ -1024,13 +1033,18 @@ impl Body<'_> {
|
||||
p.kind = Kind::Normal;
|
||||
}
|
||||
if matches!(p.kind, Kind::Code | Kind::Heading(_) | Kind::Title) {
|
||||
// The style says the size of its text, stated again or not: text of another size keeps
|
||||
// its own.
|
||||
let style_size = self.para_size;
|
||||
for i in &mut p.inlines {
|
||||
if let Inline::Text(_, f) = i {
|
||||
if p.kind == Kind::Code {
|
||||
f.code = false;
|
||||
f.font = None;
|
||||
}
|
||||
f.size = None;
|
||||
if f.size == style_size {
|
||||
f.size = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1544,7 +1558,20 @@ impl Body<'_> {
|
||||
match local(q) {
|
||||
"span" => {
|
||||
let base = self.cur_fmt();
|
||||
let fmt = get(a, "text:style-name").map_or_else(|| base.clone(), |s| self.styles.span_fmt(s, &base));
|
||||
// A size in percent is of the size it is in: its heading's, where nothing nearer
|
||||
// states one.
|
||||
let sized = Fmt { size: base.size.or(self.para_sizes.last().and_then(|(size, _)| *size)), ..base.clone() };
|
||||
let fmt = get(a, "text:style-name").map_or_else(
|
||||
|| base.clone(),
|
||||
|s| {
|
||||
let mut fmt = self.styles.span_fmt(s, &sized);
|
||||
// That size is only for working out a percent: saying nothing of size, it has none.
|
||||
if base.size.is_none() && self.styles.span_fmt(s, &base).size.is_none() {
|
||||
fmt.size = None;
|
||||
}
|
||||
fmt
|
||||
},
|
||||
);
|
||||
self.fmt.push(fmt);
|
||||
}
|
||||
"a" => {
|
||||
|
||||
@@ -1263,6 +1263,24 @@ impl Styles {
|
||||
/// The formatting of a span in text style `name` over `base`: the automatic styles' over the
|
||||
/// nearest custom character style, which the span then names ([`Fmt::extra`]'s style, by ODF
|
||||
/// name until import makes it a document style id); without one, the whole chain's.
|
||||
/// The size of text in paragraph style `name` by its named styles, not its automatic ones,
|
||||
/// which are the paragraph's own: what they say on what the document says.
|
||||
pub(super) fn named_size(&self, name: &str) -> Option<f32> {
|
||||
// The default style's: Standard's counts only for a style based on it, in the chain.
|
||||
let size = self.default_size.filter(|v| v.is_finite() && *v >= 1.0).map(|v| v.min(1638.0));
|
||||
let mut f = Fmt { size, ..Fmt::default() };
|
||||
for (_, s) in self.chain(name).iter().rev().filter(|(_, s)| !s.auto) {
|
||||
s.fmt.apply(&mut f);
|
||||
}
|
||||
f.size
|
||||
}
|
||||
|
||||
/// Whether paragraph style `name`'s named styles state a size. Those that don't leave the size
|
||||
/// to WordCraft's style for its kind, so text of any size isn't known to be the style's.
|
||||
pub(super) fn states_size(&self, name: &str) -> bool {
|
||||
self.chain(name).iter().any(|(_, s)| !s.auto && (s.fmt.size.is_some() || s.fmt.size_pct.is_some()))
|
||||
}
|
||||
|
||||
pub(super) fn span_fmt(&self, name: &str, base: &Fmt) -> Fmt {
|
||||
let chain = self.chain(name);
|
||||
let Some(at) = chain.iter().position(|(n, _)| self.is_named_text(n)) else { return self.text_fmt(name, base) };
|
||||
@@ -1462,6 +1480,10 @@ impl Styles {
|
||||
};
|
||||
let mut fmt = Fmt::default();
|
||||
s.fmt.apply(&mut fmt);
|
||||
// A percent is of what's under it: the parent's size, or with none the default style's.
|
||||
if s.fmt.size_pct.is_some() {
|
||||
fmt.size = self.named_size(odt);
|
||||
}
|
||||
// A style with no parent inherits the default style, not Standard (the document's
|
||||
// defaults here): its size, when Standard's differs.
|
||||
if s.parent.is_none() && fmt.size.is_none() && self.doc_size() != self.default_size {
|
||||
|
||||
@@ -284,3 +284,126 @@ fn objects_in_hidden_text_are_kept() {
|
||||
assert!(p.text.contains("Hidden"), "{:?}", p.text);
|
||||
assert!(p.objects.iter().any(|o| matches!(o, wordcraft_doc::para::InlineObject::BookmarkStart { name } if name == "Secret")), "{:?}", p.objects);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_headings_text_of_another_size_keeps_it() {
|
||||
// Only the size its style gives goes with the style: a smaller word in a heading came back
|
||||
// from a save at the heading's size.
|
||||
let styles = format!(
|
||||
r#"<office:document-styles {NS_T}><office:styles><style:style style:name="Heading_20_1" style:display-name="Heading 1" style:family="paragraph" style:default-outline-level="1"><style:text-properties fo:font-size="16pt"/></style:style></office:styles></office:document-styles>"#
|
||||
);
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:automatic-styles><style:style style:name="T1" style:family="text"><style:text-properties fo:font-size="10pt"/></style:style></office:automatic-styles><office:body><office:text><text:h text:style-name="Heading_20_1" text:outline-level="1">big<text:span text:style-name="T1">small</text:span></text:h></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let check = |d: &Document, what: &str| {
|
||||
let p = d.body[0].as_para().unwrap();
|
||||
let size = |run: usize| d.styles.resolve_char(p.props.style.as_deref(), &p.runs[run].props).size;
|
||||
assert_eq!((size(0), size(1)), (16.0, 10.0), "{what}");
|
||||
};
|
||||
let d = import(&package(&content, Some(&styles))).unwrap();
|
||||
check(&d, "read");
|
||||
assert_eq!(d.body[0].as_para().unwrap().runs[0].props.size, None, "the style's stays the style's");
|
||||
check(&import(&crate::odt::tests::export_model(&d).unwrap()).unwrap(), "written from the model");
|
||||
}
|
||||
|
||||
/// In a heading style with no parent, where Standard's size and the default's differ, text stating
|
||||
/// a size keeps that size.
|
||||
#[test]
|
||||
fn a_parentless_headings_text_keeps_its_size() {
|
||||
let styles = format!(
|
||||
r#"<office:document-styles {NS_T}><office:styles><style:default-style style:family="paragraph"><style:text-properties fo:font-size="10pt"/></style:default-style><style:style style:name="Standard" style:family="paragraph" style:class="text"><style:text-properties fo:font-size="14pt"/></style:style><style:style style:name="H" style:family="paragraph" style:default-outline-level="1"/></office:styles></office:document-styles>"#
|
||||
);
|
||||
for size in ["10pt", "14pt"] {
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:automatic-styles><style:style style:name="T1" style:family="text"><style:text-properties fo:font-size="{size}"/></style:style><style:style style:name="T2" style:family="text"><style:text-properties fo:font-weight="bold"/></style:style></office:automatic-styles><office:body><office:text><text:h text:style-name="H" text:outline-level="1"><text:span text:style-name="T1">One</text:span><text:span text:style-name="T2">Two</text:span></text:h></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let want: f32 = size.trim_end_matches("pt").parse().unwrap();
|
||||
let check = |d: &Document, what: &str| {
|
||||
let p = d.body[0].as_para().unwrap();
|
||||
assert_eq!(p.runs[1].props.size, None, "{size} {what}: a span stating no size has none");
|
||||
assert_eq!(
|
||||
d.styles.resolve_char(p.props.style.as_deref(), &p.runs[0].props).size,
|
||||
want,
|
||||
"{size} {what}: {:?} {:?}",
|
||||
p.props.style,
|
||||
p.runs[0].props.size
|
||||
);
|
||||
};
|
||||
let d = import(&package(&content, Some(&styles))).unwrap();
|
||||
check(&d, "read");
|
||||
check(&import(&crate::odt::tests::export_model(&d).unwrap()).unwrap(), "written from the model");
|
||||
}
|
||||
}
|
||||
|
||||
/// A percent in a heading style with no parent is of the default style's size, not Standard's.
|
||||
#[test]
|
||||
fn a_percent_in_a_parentless_heading_is_of_the_default_size() {
|
||||
let styles = format!(
|
||||
r#"<office:document-styles {NS_T}><office:styles><style:default-style style:family="paragraph"><style:text-properties fo:font-size="10pt"/></style:default-style><style:style style:name="Standard" style:family="paragraph" style:class="text"><style:text-properties fo:font-size="14pt"/></style:style><style:style style:name="H" style:family="paragraph" style:default-outline-level="1"/></office:styles></office:document-styles>"#
|
||||
);
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:automatic-styles><style:style style:name="T1" style:family="text"><style:text-properties fo:font-size="50%"/></style:style></office:automatic-styles><office:body><office:text><text:h text:style-name="H" text:outline-level="1"><text:span text:style-name="T1">One</text:span></text:h></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let d = import(&package(&content, Some(&styles))).unwrap();
|
||||
assert_eq!(d.body[0].as_para().unwrap().runs[0].props.size, Some(5.0));
|
||||
}
|
||||
|
||||
/// A heading style with no parent and a percent size is that percent of the default's size, so text
|
||||
/// stating that size is at the style's size.
|
||||
#[test]
|
||||
fn a_parentless_heading_style_in_percent_is_of_the_default_size() {
|
||||
let styles = format!(
|
||||
r#"<office:document-styles {NS_T}><office:styles><style:default-style style:family="paragraph"><style:text-properties fo:font-size="10pt"/></style:default-style><style:style style:name="H" style:family="paragraph"><style:text-properties fo:font-size="50%"/></style:style></office:styles></office:document-styles>"#
|
||||
);
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:automatic-styles><style:style style:name="T1" style:family="text"><style:text-properties fo:font-size="5pt"/></style:style></office:automatic-styles><office:body><office:text><text:h text:style-name="H" text:outline-level="1"><text:span text:style-name="T1">One</text:span>Two</text:h></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let d = import(&package(&content, Some(&styles))).unwrap();
|
||||
let p = d.body[0].as_para().unwrap();
|
||||
for r in &p.runs {
|
||||
assert_eq!(d.styles.resolve_char(p.props.style.as_deref(), &r.props).size, 5.0, "{:?} {:?}", p.props.style, r.props.size);
|
||||
}
|
||||
}
|
||||
|
||||
/// Text at its heading's size goes with the heading style, stated or not, and a size in percent
|
||||
/// is of the heading's size; text of another size keeps it, and so does a paragraph's own.
|
||||
#[test]
|
||||
fn a_headings_text_sizes_are_measured_against_the_heading() {
|
||||
let styles = format!(
|
||||
r#"<office:document-styles {NS_T}><office:styles><style:style style:name="Heading" style:family="paragraph"><style:text-properties fo:font-size="20pt"/></style:style><style:style style:name="Heading_20_1" style:display-name="Heading 1" style:family="paragraph" style:parent-style-name="Heading" style:default-outline-level="1"><style:text-properties fo:font-size="80%"/></style:style></office:styles></office:document-styles>"#
|
||||
);
|
||||
let span = |name: &str, size: &str| {
|
||||
format!(r#"<style:style style:name="{name}" style:family="text"><style:text-properties fo:font-size="{size}"/></style:style>"#)
|
||||
};
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS_T}><office:automatic-styles>{}{}{}<style:style style:name="P1" style:family="paragraph" style:parent-style-name="Heading_20_1"><style:text-properties fo:font-size="30pt"/></style:style></office:automatic-styles><office:body><office:text><text:h text:style-name="Heading_20_1" text:outline-level="1">plain <text:span text:style-name="Same">same</text:span> <text:span text:style-name="Pct">pct</text:span> <text:span text:style-name="Other">other</text:span></text:h><text:h text:style-name="P1" text:outline-level="1">own</text:h></office:text></office:body></office:document-content>"#,
|
||||
span("Same", "16pt"),
|
||||
span("Pct", "50%"),
|
||||
span("Other", "10pt"),
|
||||
);
|
||||
let mut d = import(&package(&content, Some(&styles))).unwrap();
|
||||
let sizes = |d: &Document| {
|
||||
let p = d.body[0].as_para().unwrap();
|
||||
let of = |word: &str| {
|
||||
let at = p.text.find(word).unwrap();
|
||||
let mut from = 0;
|
||||
let run = p.runs.iter().find(|r| {
|
||||
from += r.len;
|
||||
at < from
|
||||
});
|
||||
d.styles.resolve_char(p.props.style.as_deref(), &run.unwrap().props).size
|
||||
};
|
||||
[of("plain"), of("same"), of("pct"), of("other")]
|
||||
};
|
||||
let want = |t: f32| [t, t, 8.0, 10.0];
|
||||
let plain = |d: &Document| d.body[0].as_para().unwrap().runs.iter().filter(|r| r.props.size.is_some()).count();
|
||||
assert_eq!(sizes(&d), want(16.0));
|
||||
assert_eq!(plain(&d), 2, "only the two of another size state one");
|
||||
// The paragraph's own size stays with its text.
|
||||
let own = d.body[1].as_para().unwrap();
|
||||
assert_eq!(d.styles.resolve_char(own.props.style.as_deref(), &own.runs[0].props).size, 30.0);
|
||||
// A change to the style reaches what goes with it.
|
||||
let id = d.body[0].as_para().unwrap().props.style.clone().unwrap();
|
||||
std::sync::Arc::make_mut(&mut d.styles).get_mut(&id).unwrap().chr.size = Some(24.0);
|
||||
assert_eq!(sizes(&d), want(24.0));
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# File-format parity with Microsoft Word
|
||||
> **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-05 · **Change:** trivial (RTF footnotes, endnotes and comments, #1053); previously trivial (ODT pictures offered in several formats); 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 trivial (ODT table cell formulas as formula fields); previously trivial (ODT custom shapes drawn from their own geometry); previously trivial (ODT bibliography marks as citations of sources); previously trivial (ODT linked text boxes); previously trivial (ODT picture crops and contour wrapping); previouslyLine truncated
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-05 · **Change:** trivial (RTF footnotes, endnotes and comments, #1053); 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 trivial (ODT table cell formulas as formula fields); previously trivial (ODT custom shapes drawn from their own geometry); previously trivial (ODT bibliography marks as citations of sources); previously trivial (ODT linked text boxes); previously triviLine truncated
|
||||
> **Legacy form update:** 2026-10-22 · trivial: field names and selected drop-down labels decode numeric XML character references once.
|
||||
|
||||
Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the
|
||||
@@ -320,6 +320,7 @@ CSL and library import are separate work.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 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. |
|
||||
| 2026-11-05 | trivial | ODT: a frame offering one picture in several formats (LibreOffice's `.svm` metafile, then a PNG) shows the first WordCraft can read; it took the first whatever it was, and the picture was missing on screen and gone once the paragraph was rewritten An object's replacement picture coming before it is kept too. A readable picture after an unreadable replacement is shown, wherever the object comes, and a save from the model keeps both. |
|
||||
| 2026-11-05 | trivial | DOCX paragraph `w:adjustRightInd` keeps its default and explicit values in schema-ordered repeated saves |
|
||||
| 2026-11-04 | trivial | REF context update: 2026-11-04 · #4609 retains the label and template actually rendered by an ancestor list instance; implicitly counted levels remain distinct from explicitly rendered paragraphs. Synthetic instance-override and field-update regressions; no new native Word qualification or readiness change. |
|
||||
|
||||
Reference in new issue
Block a user