Widow and orphan control a file doesn't state is open in the standard
(it refers to XSL, whose initial value is 2); LibreOffice reads it as
off. It is now a profile case (widowControl), and WordCraft writes it on
its default style either way.
LibreOffice measures tab stops from the paragraph's indent when its
TabsRelativeToIndent setting is on (its default), default stops too
(measured). WordCraft's layout now counts default stops from the indent
for such documents, and writes them back measuring from the indent
instead of from the margin, which moved LibreOffice's default stops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by comparing WordCraft's own documents in LibreOffice and
bisecting settings.xml (observed only, black-box):
- The settings set's name, ooo:configuration-settings, is a qualified
name: without xmlns:ooo declared LibreOffice ignored every setting
WordCraft wrote (its Word layout rules, tab measuring).
- A set LibreOffice reads must be complete: a setting missing from it
gets its old pre-OpenOffice value, which changes the layout. WordCraft
now writes LibreOffice's current layout defaults (as a new LibreOffice
document has them), overlaid with Word's layout rules for documents
WordCraft lays out as Word does (not for ones opened from ODF), and
its own choices.
- How the space between paragraphs adds up follows a file's
AddParaTableSpacing (false: the larger, as Word); unstated, the ODF
profile's new sumParagraphSpacing (WordCraft: the larger, LibreOffice:
the sum). WordCraft writes it as it lays the document out.
WordCraft-origin documents in LibreOffice: 142k -> 80k differing pixels
over 42 documents; the LibreOffice corpus: no regressions, 40k -> 39.6k.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by saving WordCraft's own documents as ODT and opening them in
LibreOffice (42 documents: templates, themes, one per feature).
- Table styles: ODF has no table styles of Word's kind, so each cell now
states what the style gives it (fills, its borders, bold header text,
its paragraph spacing), under the paragraph's own styles as layout
applies them (ECMA-376 §17.7.2; StyleSheet::table_para_patch and
table_char_patch). The style's border mapping moved to
styles::part_borders, shared with layout.
- Tables read from ODT no longer get Word's Table Grid style, whose
single spacing changed their text.
- Columned sections say whether their columns end balanced
(text:dont-balance-text-columns, §20.428, read as its name says; its
value descriptions contradict it). Unstated, the ODF profile decides
(balanceSectionColumns: LibreOffice balances, WordCraft as Word).
SectionProps::balance_columns carries it to layout.
- SmartArt diagrams are saved as a draw:g of their shapes and text (with
an invisible frame keeping the graphic's box), which LibreOffice draws
as WordCraft does.
LibreOffice round-trip corpus: 40k differing pixels over 125 documents
(was 76k), 42 identical; WordCraft-origin documents 277k -> 145k.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODF 1.3 §20.206–20.209 define page margins by referring to XSL's
(§7.10), without saying whether XSL's initial value 0 comes with the
reference; LibreOffice uses 2 cm. The margin a page layout leaves out
is now the profile's `pageMargin`: WordCraft 0 (XSL's), LibreOffice
2 cm, or custom (File › Options › Advanced › OpenDocument files,
`file.options` `odf.pageMargin`). A stated margin is the file's under
every preset. With the LibreOffice preset, LibreOffice's CV, sans-serif
business letter and one-page résumé templates match LibreOffice again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
What ODF 1.3 defines, including XSL's initial values for fo: properties,
is followed whatever the settings: unstated page margins are now 0, and
padding without a border takes room in every ODT document. What the
standard leaves to the application (Part 3 §16.2) is an OdfProfile:
WordCraft's own choices by default, LibreOffice's, or custom values.
The profile fills in only what a file leaves unsaid; stated values win
under every preset (tests_more/profile.rs).
Cases: header height, how its height and spacing combine, dynamic
spacing, tabs from the indent, column gap, shape fill, line and
hairline, page frames against a paragraph, in-line frames against a
paragraph or page, and lines of only pictures.
- doc: odf_profile module; Settings::odf (OdfLayout) replaces odf_layout
- formats: import_with / import_flat_with take a profile
- engine: Prefs::odf, file.options `odf`, opened files follow it
- ui: File > Options > Advanced > OpenDocument files (presets, custom cases)
- cli: WORDCRAFT_ODF=wordcraft|libreoffice
- docs: odf-interpretation.md lists every case (law, open, writer
choices, Google Docs and ONLYOFFICE findings); parity docs, gaps,
ROADMAP updated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Form controls:
- Placement: a control hanging from the baseline (as LibreOffice places
those it makes) was saved with no placement. LibreOffice then puts it
at the line's top, so every control row shrank. Its raise is now kept
(`ContentControl::form_raise`, read as in-line frames are) and
written, standing on the baseline when it had none.
- Multi-line fields: these were saved without LibreOffice's `MultiLine`
property, so they showed one centred line. They now carry it.
- Text fields get LibreOffice's flat border.
- Also: `pt()` no longer writes "-0pt".
Custom shapes:
- A shape drawn from its own geometry was saved as WordCraft's path of
it, and a preset WordCraft draws as a rectangle (a cloud) was saved as
a rectangle. The geometry as read (type, path, equations, handles) is
now kept (`ShapeExtra::odf_geometry`) and written back, mirrored as
the shape now is.
- `shape.change` and the ink-to-shape conversion drop the kept geometry,
since the shape is then another kind. `ShapeExtra` holds it as an
`Arc<String>` to keep `Placed::Shape` small.
- The reader's `end` hands skipped elements to `end_skipped`.
Results:
- The forms document round-trips pixel-identical (2,949 pixels before),
with its text the same.
- The shape presets fall from 2,467 to 1,220. The rest is LibreOffice's
hairline outline, which WordCraft keeps as 0.5 pt.
- Across the corpus the round-trip difference falls from 45k to 41k
pixels. 42 of 125 documents round-trip pixel-identical, 92 with their
text the same, and none gets worse.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Charts: on save, WordCraft wrote each chart anew from its chart model
(type, data, title, legend, labels), losing the look it doesn't model:
fonts and sizes, axis and grid lines.
- The chart object as read is now kept with the chart (`Chart::source`
in the flow model, `Graphic::source` in the document).
- `chart::redraw` keeps it when the chart is drawn again on open or
resize.
- The ODT writer writes it back whole while it still reads as the same
chart. An edited chart is written from the model, as before; DOCX
still writes charts from their model.
- The chart documents now round-trip pixel-identical (chartcol: 5,917
pixels to 0).
LibreOffice's serif business letter (4,223 pixels to 116):
- A text frame outlined by a drawn stroke (`draw:stroke`, a 0 width a
hairline) and no `fo:border` lost its outline. It is now the frame's
line; an explicit "none" border still wins.
- A header or footer whose style states no height, or that has no style,
was 0 tall. LibreOffice makes it 0.5 cm (measured by varying the
height), so the text had moved up 1.5 pt.
Across the corpus the round-trip difference falls from 60k to 47k
pixels, and 41 of 125 documents round-trip pixel-identical. No document
gets worse.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Word's Font › Emphasis mark (a mark over or under each character, used
to stress East Asian text) wasn't modelled. DOCX `w:em` and ODT
`style:text-emphasize` were dropped.
- Model: `CharProps::emphasis` (dot, comma, circle, dot below, or none
to turn off a style's).
- Layout: a mark is drawn over (or under) each character, and the line
is made taller to fit it.
- `format.emphasisMark {"mark": …}` sets it, and Format state reports
it. The catalog lists it under Home › Font.
- DOCX: `w:em` is read, and written in its place in `w:rPr` (after
`w:cs`, before `w:lang`).
- ODT: `style:text-emphasize` maps to it: an accent is the comma, a
disc the dot, a dot below the under-dot.
Also fixes two problems from the previous commit (ODT variables):
- Field updates blanked a SET field's stored result. The layout
already draws nothing for SET, so its result stays as it was, as
`ask_and_set_fields_draw_nothing` pins.
- A clippy lint (manual char comparison) failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- ODT variables (`text:variable-set`, `text:variable-get`, §7.4) were
read as their text. A set is now Word's SET field, followed by a REF
when it is displayed; a get is a REF. They are written back declared
(`text:variable-decls`), the sets hidden with a get after them.
- The engine now evaluates SET (ECMA-376 §17.16.5.58). It shows
nothing and gives its name a value. REF to a name no bookmark has
shows that value; a REF before the SET shows the last value set, as
Word's bookmark does after an update. This works for Word documents
too.
- Custom character styles now keep what they turn off under a parent
that turns it on (`fo:font-weight="normal"`…), as paragraph styles
already do.
A LibreOffice document with variables round-trips with the same text,
and the saved file validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice ships 78 text templates (letters, faxes, agendas, reports,
CVs). Round-tripping them through WordCraft exposed these problems:
- Placeholders (`text:placeholder`, "<Recipient's name>") were
printed text. They now open as plain-text content controls showing
them, so a click selects one and typing replaces it. Their
description becomes the control's title. They are written back as
placeholders of their type while they still show; what was typed is
written as text.
- A fixed date with neither a value nor text was lost. It is now a date
field.
- `text:author-name` opens as USERNAME, which the engine left empty.
USERNAME and USERINITIALS now show the user's name and initials
from File › Options; a quoted text after the name is shown instead.
- A "First Page" master followed by another (`style:next-style-name`)
put the running header on the first page. It is now a section with a
different first page: its own (empty) header on page 1, the next
master's after that.
- A page layout without a size was Letter. It is now the default
page's, the user's region's, as LibreOffice uses its default paper.
ODT import takes the regional page like the other importers.
- Style-only templates (no `content.xml`) failed to open. They now
open empty with their styles.
- Table columns are now written with their share of the table
(`style:rel-column-width`) too. A table filling the margins of a
narrower page keeps its proportions in LibreOffice.
All 78 templates now open and round-trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Hidden form controls (`form:hidden`: a name and value the form sends,
not shown) were dropped. They are now kept verbatim, with their
properties, and written back into the form with ids of their own.
- OpenDocument text templates (`.ott`) weren't opened. They now open
as documents, and save as templates: the template's media type goes
in `mimetype` and in the manifest's root entry (ODF 1.3 Part 2 §3.3).
This works with a password too. `.ott` is listed wherever `.odt` is:
open and save formats, file dialogs, export, the web app and its
media type.
A LibreOffice template round-trips with the same text. 300 fuzzed
documents exercising custom shapes, cell formulas, kept parts, turned
text and forms caused no crashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT cell formulas (`table:formula`) were lost, and only their value was
kept as text.
- Reading: LibreOffice's formulas, in its own syntax
(`ooow:sum <A1:A3>`, `<B1>*<B2>+<B3>`, `mean`, `min`, `max`,
`product`, `abs`, `sign`, `count`, `add`/`sub`/`mul`/`div`/`pow`, lists
joined by `|`), open as Word formula fields (`=SUM(A1:A3)`) showing
the cell's value. Update Field recalculates them.
- Writing: formula fields in cells are written back as cell formulas
with their value. ABOVE, LEFT, BELOW and RIGHT become the cells they
name.
- Cells holding a plain number are written as numbers, so LibreOffice
calculates with them. The `ooow` prefix is declared; without it,
LibreOffice reports "Expression is faulty".
A formula one side can't say keeps its value as text. A LibreOffice
table of sums round-trips with the same text, and the saved file
validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT citations (`text:bibliography-mark`) were read as their text, and
the sources they carry were lost. So an ODT bibliography had nothing
to update from.
- Reading: each mark is now a `CITATION` field of its source, which
becomes one of the document's bibliography sources (the first mark
of an identifier gives it). The identifier is the tag, the type the
kind, plus author, title, year, publisher, address, journal, volume,
pages, URL, editor, edition, number and month.
- Writing: a `CITATION` of a known source is written back as a mark
carrying the source.
References › Update rebuilds an ODT bibliography from them. A
LibreOffice citation and bibliography round-trip pixel-identical and
validate against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT custom document properties (`meta:user-defined` in `meta.xml`) were
dropped, and the fields showing them or a user field were read as
plain text.
- Properties: `meta:user-defined` values (string, number, true/false,
date) are now WordCraft's custom document properties, and so are
user field declarations (`text:user-field-decl`).
- Fields: `text:user-defined` and `text:user-field-get` are Word's
`DOCPROPERTY` fields, which WordCraft updates from those properties.
- Writing: custom properties go back into `meta.xml`, and a
`DOCPROPERTY` naming one is written as `text:user-defined`.
A LibreOffice document with a custom property and a user field
round-trips pixel-identical, valid against the schema. Its user field
comes back as a document property.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT index marks (`text:alphabetical-index-mark`) were dropped. So
References › Update Index on an opened ODT index found no entries and
left the old list in place. Now:
- a mark at a point is Word's `XE` field, its `text:key1`/`text:key2`
the levels above its entry;
- a mark over text (`-mark-start` … `-mark-end`, in one paragraph) is
an `XE` field whose entry is the marked text, which stays text;
- `XE` fields are written back as point marks.
Engine: Update rebuilt only lists in WordCraft's own form, a paragraph
holding the field. A list held as a field around its entries, as ODT
and Word files hold an index, bibliography or table of figures, is now
first made the field's own paragraph, then rebuilt. That happens only
when the field starts its paragraph, so no document text is replaced.
A LibreOffice document with an index mark round-trips pixel-identical,
the mark kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT form controls (ODF 1.3 §13: controls in `office:forms`, each
placed in the text by a `draw:control`) were dropped on open, values
and all. They now open as WordCraft content controls, each with its
value, entries and selection; its name becomes the tag and its label
the title:
- text fields and text areas: plain text controls;
- check boxes: check boxes;
- radio buttons: check boxes with round symbols;
- list boxes: drop-down lists;
- combo boxes: combo boxes;
- date fields: date controls.
Saved as DOCX they become Word content controls.
Content controls of those kinds in the body are written back as form
controls with a `draw:control` anchor; in headers and footers they stay
their text. LibreOffice shows the round-tripped controls with their
values, and the content validates against the ODF 1.3 schema.
Not read: buttons, images, hidden and other controls. Controls' sizes
and looks aren't kept.
Flow model: new `Inline::Control`, kept only by flows that ask for
boxes (ODT); other formats get its text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A password-protected ODT (ODF 1.3 Part 2 §3.4) failed to open. The
package is now decrypted with the password into a plain one, which the
reader opens as before.
- Supported: AES-256-CBC with a SHA-256 or SHA-1 start key, PBKDF2
with HMAC-SHA-1, and SHA-256 or SHA-1 1K checksums (LibreOffice's
encryption).
- Blowfish (the old default) is refused with a message naming it.
- A missing or wrong password gives the same errors as Word files, so
the app asks for the password.
- Hostile parameters are bounded: iteration count, IV size, ragged
data, inflated size.
Saving with a password (and saving a document opened with one) writes
the ODT encrypted as LibreOffice does. Every file but the `mimetype`
and manifest is deflated and encrypted, each with its own salt and IV,
and the manifest validates against the ODF manifest schema.
LibreOffice opens WordCraft's encrypted files with the password and
refuses a wrong one; WordCraft opens LibreOffice's.
No new dependencies: the AES, SHA, HMAC, miniz and getrandom crates
the workspace already uses for Word encryption.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice writes an embedded object's replacement picture in its own
metafile format, which WordCraft can't decode. So an embedded
spreadsheet (`draw:object`, ODF 1.3 §10.4.6) or OLE object
(`draw:object-ole`, §10.4.7) vanished on open, and saving lost it.
Such objects are now kept whole: their files, media type (from the
manifest or the sub-document's body) and replacement picture.
- Display: the replacement picture when WordCraft can show it, else a
placeholder of the object's size drawn in code (ruled for a
spreadsheet).
- ODT save: written back as the same object, with its manifest entries
and original replacement. LibreOffice renders a round-tripped
spreadsheet pixel-identically.
- Document model: the files live in `passthrough` under `odf-object/`,
linked from the picture's `ole` by a marker. The DOCX writer skips
the marker and writes the picture alone.
Flat ODT, a fix to the chart commit: inline sub-documents lacked the
required `office:mimetype`, so the schema rejected flat files holding
charts. Sub-documents now go inline as full `office:document`s (media
type, styles, settings) and are split back into their files on
opening. Replacement pictures and OLE files go inline as base64. A
picture's data is matched to the element that holds it, no longer to
the last `draw:image` before it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the chart commit: the engine drew opened charts only at the
paragraph level, so a chart that was a member of a `draw:g` group
opened blank. The pass now goes into groups (bounded depth).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A chart object (`draw:object` whose sub-document is an `office:chart`,
ODF 1.3 §10.4.3, Part 3 §11) used to open as its replacement picture.
It is now read into WordCraft's chart model: column, bar (stacked or
not), line, area, pie, doughnut and scatter charts, with categories,
series names and values (gaps included) taken from the chart's own
table through the series' cell range addresses, plus title, legend
position and data labels. The engine draws opened charts in the
document's theme colours, and Chart Design's commands edit them.
Charts WordCraft can't edit as they are (3D, percent-stacked, mixed
plot types, a second value axis, radar and the like) keep their
picture.
WordCraft charts are written as ODF chart objects shaped like
LibreOffice's: a data table, each series and slice in its accent
colour, pies clockwise from the top. They validate against the ODF 1.3
schema, and LibreOffice draws them like WordCraft does. In flat
documents they are written inline as an `office:document`.
Bug fixed: flat ODT reading ended the body at the first nested
`</office:body>`, so an inline sub-document dropped the text after it.
Element lookup now follows nesting, with a bounded depth.
Flow model: new `Inline::Chart`, kept only by flows that ask for
boxes (ODT); other formats still get the alt text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A flat ODT holds the whole document in one `office:document` element
(ODF 1.3 Part 3 §2.1.2), its pictures inline as base64
`office:binary-data` and its formulas inline in their `draw:object`.
It is opened by taking it apart into a package's files (content,
styles, meta, settings; pictures and formulas as entries, each part's
root keeping the document's namespaces) and reading that package, and
saved by putting a package's files together: one set of automatic
styles, every part's namespaces declared once on the root, pictures and
formulas inline without the link attributes.
`.fodt` opens and saves from Open, Save As, the CLI and the web app.
LibreOffice's own flat documents round-trip as their packages do, and
the written files validate against the ODF 1.3 schema.
The parity doc's ODT fidelity is re-estimated at ~80%, and its status
line, which hadn't been updated since the Asian fonts change (a text
replacement that matched nothing), now lists every change since.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Fields that were read as plain text now open as Word's, so Update Field
recomputes them, and are written back as ODF's:
- dates and times in their date or time style (`number:date-style`,
`number:time-style`) ↔ `DATE`/`TIME \@ "picture"`: day, month (by
number or name), year, weekday, hours (12-hour with AM/PM), minutes,
seconds and the text between, both ways; data styles are written as
automatic styles and the `number` namespace declared;
- sequences numbering figures and tables (`text:sequence`) ↔ `SEQ` with
its number format; on save the sequences are declared
(`text:sequence-decls`); on open a bookmark named by the sequence's
reference name covers the caption's label and number ("Figure 1"),
what references to it show;
- chapter fields (`text:chapter`) ↔ `STYLEREF "Heading n"`, by name or
number (`\n`);
- reference marks as bookmarks, and references to them and to sequences
(`text:reference-ref`, `text:sequence-ref`) as `REF`/`PAGEREF`.
Captions renumber, references to them and chapter fields refresh after
Update Field (engine test `odt_fields.rs`). A LibreOffice document with
all of them round-trips 13 px apart with the same text; the written
content and styles validate against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Word's date-time pictures (`DATE \@ "…"`) put literal text in single
quotes, two quotes in a row standing for one: `H' o''clock'` shows
"8 o'clock". The formatter read the quotes as text and the letters
inside as instructions (an `a` became AM or PM). Quoted text is now
shown as it is; an unclosed quote runs to the end.
Regression test: `date_pictures_keep_quoted_text`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A bookmark over text (`text:bookmark-start`…`text:bookmark-end`, in one
paragraph or across several) was read as a point at its start, and a
Word bookmark over text was written as a point: a reference to it then
updated to nothing. The flow model gets the end of a bookmark
(`Inline::AnchorEnd`, in flows asked for fields, as ODT's is), the
document gets the bookmark's start and end where they are, and the
writer writes a start and an end (a point, `text:bookmark`, when it
ends where it starts). A bookmark started twice, ended twice or ended
before it starts is kept once, as a point when it has no end after it.
References to bookmarks, LibreOffice's references to headings included,
now update to the bookmarked text (engine test `odt_fields.rs`, through
a save and reopen).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tables of contents, tables of figures (`text:illustration-index`,
`text:table-index`), alphabetical indexes and bibliographies open as
WordCraft's: Word's `TOC` (levels from the source's
`text:outline-level`; `\c` and the caption name for figures), `INDEX`
and `BIBLIOGRAPHY` fields over their entries as last generated. With a
title the field goes in the title paragraph, as References › Table of
Contents inserts one; without, it spans the entries (new flow inlines
`FieldStart`/`FieldEnd`, mirroring the document's range fields). Update
Table then regenerates them from the headings (engine test
`odt_toc.rs`, titled and untitled, through a save and reopen).
LibreOffice's Contents 1–10, Contents Heading and Figure Index 1 styles
are WordCraft's TOC 1–9, TOC Heading and Table of Figures, both ways,
also when a file names them without defining them. WordCraft's tables
are written as ODF indexes with their source and entry templates, so
LibreOffice updates them, the entries' links in the Index Link style.
User and object indexes are read as their paragraphs; index marks are
not read yet.
Found on the way, both shown by the table of contents' round trip:
- a style based on LibreOffice's generic Heading style (as its headings
and index headings are) lost that parent on save, and with it its
font and spacing;
- a paragraph's tab stops were measured from its own indent only, not
from an indent its style gives (LibreOffice's `TabsRelativeToIndent`),
and a named style's stops not from its indent at all.
A LibreOffice document with a table of contents, a table of figures and
an alphabetical index round-trips pixel for pixel (it was 29,717 px
apart and a page short). The parity doc's ODT fidelity is re-estimated
at ~72%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A hyperlinked TOC wrote each entry's page number as plain text inside the
link. Word writes it as a PAGEREF _Toc… \h field, so Update Field in Word
moves the number with its heading; with plain text the numbers stay what
they were when the file was saved.
Entries that link to a bookmark now end with that field (result: the page
number). Entries without a link and \n levels are unchanged. The DOCX span
test looks for the TOC's own end, the last one, since PAGEREF fields now end
inside it.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Found while testing crates/engine/src/cmd/mailings/recipients.rs:6-18 (csv_separator)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #4734: the print job's PDF leaves out link annotations
craft-print refuses to fit a page with annotations to the paper ("flatten page annotations
before using shrink-to-fit"), and the desktop app always prints with ShrinkToFit, so any
document with a hyperlink or a TOC (its entries are links) failed to print. Links print
nothing; `PdfOptions::links` (default on) lets the print job leave them out. Exported PDFs
keep them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4734: review — the print note is in every desktop column; gaps and progress log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4734: doc wording: the print PDF omits link annotations; hardware still to validate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4734: only the printer's PDF drops links; Save these pages as PDF keeps them
Session::print_job_pdf is what ui.printSubmit sends to the spooler, without
link annotations; Session::print_pdf (file.printPdf) keeps them again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(engine): saturate page parameters on 32-bit and wasm (#3676)
The file.exportPng and edit.goto paths cast a u64 page to usize and subtracted one. On 32-bit and wasm, page 2^32 truncated to 0 and the subtraction wrapped to page 1 (a debug panic).
Add p::page_index, which saturates instead of wrapping, and use it in both engine paths. The MCP render path already has the same guard on main; the new tests also pin that behavior.
Regression tests cover the helper, Go To, PNG export and the MCP render path.
* fix(engine): guard caret.set page cast too (#3676)
caret.set takes a 0-based page; pg as usize truncated on 32-bit and wasm and could select page 0. Use the same checked conversion and add a regression test.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #3631: index laid-out objects during connector rerouting
* docs: keep the gaps.md note in the status line and revision table
* docs: drop a stray blank line
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #4848: corporate authors in bibliographies
A Word source whose author is an organisation (Create Source › Corporate Author)
stores it as `b:Corporate`, which the reader ignored: the source opened with no
author, citations fell back to the title and bibliography entries started with
the year. A bare name typed into Author was split into "Organization, W. H.".
- Source gains `corporate`, read from and written as `b:Corporate` (in place of a
name list: a role holds one or the other, ECMA-376 CT_NameOrCorporateType).
- Citations and bibliography entries write it as it is in every style; it gives
the source's tag and its place in an author-sorted list, and untagged sources
are matched by it.
- APA puts its full stop after a group author ("World Health Organization.
(2021).") instead of relying on the initials for it.
- Create Source has a Corporate Author box under Author; Edit Source opens with
it checked when the source has one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6DxS4tKmWSLCiDPvb4G
* Match untagged sources by corporate author before personal author
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M12: preserve custom-property bookmark links (#4767)
* M12: preserve bookmark links across property renames (#4767)
* M12: align custom-property preservation status dates (#4767)
* docs: keep the gaps.md note in the status line and revision table
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Row, column and cell settings applied to the caret's row, column and cell only, so
selecting the whole table and unticking "Allow row to break across pages" changed
one row and the others still split across pages.
- table.properties applies row height and rule, allow break and header row to every
selected row, the column width to every selected column, and the cell width and
vertical alignment to every selected cell (the caret's with nothing selected), in
one undo step.
- The dialog shows "Allow row to break" and "Repeat as header row" as mixed when the
selected rows differ; left alone they change nothing, clicked they set every row.
- The row-height rule box is named by "Specify height" for screen readers.
Claude-Session: https://claude.ai/code/session_01LvG6DxS4tKmWSLCiDPvb4G
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M2 #4903: step each selected run from its own font size
* docs: keep the gaps.md note in the status line and revision table
* docs: drop a stray blank line
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #4411: captions in a style based on Caption are cross-reference targets
Cross-reference and Table of Figures found captions only by the exact
Caption style, so a caption restyled with a style based on Caption (a
label's own "Caption Table") was left out: of five tableX captions,
only the two still in Caption were offered. A caption is now a
paragraph whose style is Caption or based on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4411: test captions two styles from Caption and the Table type
Covers a style based on a style based on Caption, a Table caption in
such a style offered under the built-in Table type, and a caption
label used only in a style not based on Caption staying out of the
cross-reference types.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #4086: highlighting at a caret leaves the word alone
* #4086: the highlight toggle reads pending caret formatting; word formatting updates it
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Backspace after a floating shape's anchor joins paragraphs and keeps the shape (#4592)
* wc-upkeep-6: protect Backspace joins and markers
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 4757: keep Arabic base letters when removing marks
* docs: keep the gaps.md note in the status line and revision table
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1744: WIP
* #1744: Select Table copies and cuts the whole table
Copy of a whole-table selection used to keep only the cells' paragraphs
(copy_blocks flattened a cross-cell range), and Cut only cleared the text.
Now Copy puts the table on the clipboard and Cut removes the table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1744: selected table rows and cells copy as a table; Cut clears their text
Copy of whole rows, cells of a row or a block of cells puts a table of just
those cells on the clipboard (Table::slice). Cut of rows or cells clears their
text and keeps the rows and cells, as in Word (Word check A).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1744: review fixes
Table::slice widens every row to a merged cell partly selected (no ragged
copy); an empty selected table or cell copies and cuts; a text selection over
exactly a nested table copies it; one selected_table per Cut; tests for
slice merges/widths/controls, tracked and locked cuts, and the text route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1744: Cut clears every cell Copy takes; Spike stays off for empty cell selections
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* #1744: a copied part of a percentage-width table keeps a point width
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* ROADMAP: tidy merged header
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* ROADMAP: keep only the #1744 rows over main's version
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* ROADMAP: remove conflict markers; keep only the #1744 rows over main's version
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* #1744: rename the range Cut helper clear_range_cells (main added clear_cells)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Remove conflict markers from ROADMAP.md; leave main's header line alone
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* cargo fmt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Import Path in table structure after merging main
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A paragraph with keep with next measures the paragraph after it before it is
placed (next_first_line). That measurement called layout_para directly, so
every paragraph following a heading was laid out twice in each layout pass:
once to measure it and once, through the cache, when it was placed.
The measurement now goes through the layout cache under the key
para_labelled uses (Ctx::para_ahead) when its inputs are the same: no
proofing, no typed word, not inside a text box, no inline text boxes that
resize, the body's tracked-deletion view. Otherwise it is laid out on its
own as before. It counts nothing (list labels, equation numbers and the
page-total flag are restored), so pagination is unchanged.
Adds crates/engine/examples/export_bench.rs, which saves a long generated
document (the sample document 200 times, 259 pages) through io::save_bytes
and prints wall times:
cargo run --release -p wordcraft-engine --example export_bench -- pdf 200 9
PDF export on that document, interleaved with main (Ryzen, 16 threads):
median 286 ms -> 256 ms (-10.6 %). The PDF bytes are identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>