Commit Graph
722 Commits
Author SHA1 Message Date
Claude fdbc3322b3 ODT: unedited tables, pages, sections and properties written as read; no-edit loss gate
More of what WordCraft doesn't change is written back as it was read:
tables; the document properties it doesn't model (template, statistics,
editing time, printing); the declarations opening the text and the
document's scripts; page layouts and master pages while the page setup
is unchanged, each section starting with the master page it started
with; named sections around unedited paragraphs. Package files and
folders kept markup names (a style's fill picture, a header's embedded
object) are written with it, with their manifest entries.

Files are saved as ODF 1.3; markup kept from an older file is brought to
1.3's meaning (a gradient's or hatch's unitless angle was tenths of a
degree). WordCraft's own drawings state style:flow-with-text, which the
standard leaves open.

odt::tests_more::lossless saves seven samples of our own without edits
and fails on anything lost (nothing is allowed), and checks that edits
keep the saved parts well-formed.

Over the 133-file corpus the round-trip difference falls from 191k to
51k pixels; 95 files come back pixel-identical (62 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-03 13:24:09 +00:00
Claude a6ba7321db ODT: what isn't edited is written back as it was read
A paragraph nothing changed since it was read (by a fingerprint of how it
was modelled) is written with its own markup, unknown elements and
attributes included, together with the automatic styles, common styles,
font faces, namespaces and package files it names. While no style or list
definition changed, styles.xml's office:styles is the file's own (unused
list styles, note and line numbering configuration, attributes WordCraft
doesn't read); drawing definitions and the default graphic style stay
WordCraft's, as drawings are written from the model.

Paragraphs holding drawings, comments, tracked changes or ranged marks,
or starting a page, are written from the model, as is everything edited.
Kept markup lives in memory only (wordcraft_doc::kept, 64 MB at most) and
is never part of the document's content.

Over the 133-file corpus the round-trip difference in LibreOffice falls
from 528k to 191k pixels; 62 files come back pixel-identical (46 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-03 10:39:40 +00:00
Claude 5063000781 M5 #19: ODT keeps table styles, SmartArt and section column balance
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
2026-11-02 23:24:24 +00:00
Claude 7b25253286 M5 #19: unstated page margins are an ODF interpretation setting
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
2026-11-02 22:31:49 +00:00
Claude 8d5485c8bd M5 #19: ODF interpretation profile (WordCraft / LibreOffice / custom)
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
2026-11-02 20:54:05 +00:00
Claude bc74bc23f5 Gap 19: ODT form controls keep their place and lines; custom shapes their geometry
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
2026-11-02 19:49:57 +00:00
Claude 90898f454b Gap 19: ODT charts keep their look; frame outlines; default header height
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
2026-11-02 17:11:20 +00:00
Claude 807fe91724 Gap 19: ODT tables keep header rows only where they repeat
Found with LibreOffice's agenda templates. Saving wrote a table's first
row as `table:table-header-rows`, which LibreOffice repeats at the top
of each page, so a table crossing a page showed its first row twice
("<bring>" twice). The cause: the flow model marks a first row as a
header when the table style's look has a header row, which Word's look
has by default. That only affects the style's formatting, and HTML and
Markdown still rely on it for heading cells.

- The flow table now carries how many rows repeat
  (`TableExtra::header_rows`, Word's Repeat as header row).
- The ODT writer writes only those rows as header rows.
- That extra block is now always kept for ODT flows, where it had been
  dropped from tables without an alignment, width or row height.

All 16 agenda templates now round-trip with their text the same; the
modern and simple ones fall from about 5,200 differing pixels to 145.
Across the corpus, documents with the same text after a round trip rise
from 75 to 91 of 125, and the round-trip difference falls from 82k to
60k pixels.

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
2026-11-02 16:11:20 +00:00
Claude be2666da15 Gap 19: ODT padding without a border, unequal columns, column breaks
Found with LibreOffice's report templates and one-page résumé.

Padding without a border:
- LibreOffice gives a paragraph's padding room on every side, with or
  without a border; measured, 0.5 in of padding and no border moves the
  text 0.5 in in and down. WordCraft dropped it.
- The reader now keeps it (an invisible border holding the space; a new
  `Borders::takes_room()`), and the writer writes it back.
- The layout adds it to the spacing or indent under the document
  setting, now renamed `Settings::odf_layout` to cover all of
  LibreOffice's layout rules. `ResolvedPara::padding` carries it, so
  Word documents are unchanged.

Columns:
- Unequal columns (`style:column` relative widths and indents, on pages
  and sections) were made equal. They now keep their widths and are
  written back as shares. The writer leaves out `fo:column-gap` beside
  them, because LibreOffice ignores a page's column widths when a gap is
  stated (checked).
- Columns with no stated gap have none, where they had Word's 0.5 in.
- A paragraph starting a column (`fo:break-before="column"`) now starts
  with Word's column break, written back on the paragraph.

The report templates round-trip to 0-11 differing pixels (from up to
9,600), and the résumé to 24 (from 6,286) with its text the same.
Across the corpus the round-trip difference falls from 155k to 82k
pixels. WordCraft's own rendering difference falls from 469k to 428k,
and 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
2026-11-02 15:39:32 +00:00
Claude adfcbcbf7c Gap 19: ODT pictures alone in a paragraph keep its size and their raise
Found with LibreOffice's elegant agenda:
- Paragraph size: a paragraph holding only pictures lost its own font
  size, since the flow model's picture runs carry no formatting, so its
  line grew. Such a paragraph (`Para::textless`) now keeps its formatting
  as the paragraph mark's, which its pictures' runs take too. Read and
  written as for empty paragraphs.
- Raise: an in-line frame's raise was dropped when a parent style gave
  it a relation no in-line frame has (the Graphics style's
  `style:vertical-rel="paragraph"`). It is now measured from the baseline,
  as LibreOffice reads it.

The agenda's round-trip difference falls from 6,542 to 1,331 pixels, and
two business fax templates' from about 5,300 to 270 and 350. Across the
corpus it falls from 185k to 155k. No document changes page count.
WordCraft's own rendering of a picture-only line is still taller than
LibreOffice's, a layout difference left for later.

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
2026-11-02 14:07:54 +00:00
Claude 2f08e8bb01 Gap 19: ODT frames take their anchor and place from their style
A frame that doesn't state its anchor or position now takes them from
its graphic style chain (ODF lets `style:graphic-properties` carry
`text:anchor-type`, `svg:x` and `svg:y`).

LibreOffice's business card template relies on this:
- its Labels style holds the card in line;
- its Graphics style puts the credit at the top of the page.

WordCraft had floated the card, so a save scrambled the layout and added
a page.

Across the 125-document corpus, no document now changes page count
through a save (3 did), and the round-trip pixel difference falls from
333k to 241k. 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
2026-11-02 12:51:36 +00:00
Claude 6a62c6142f Gap 19: docs: variables, emphasis marks, page backgrounds, tables, default size
gaps.md row 19 now lists what's left in ODT:
- turned characters;
- conditional text;
- control looks;
- embedded spreadsheets' contents;
- dates in non-US locales.
The ROADMAP progress log has the entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 11:09:38 +00:00
Claude bc5d9f9fa5 Gap 19: docs: ODT checked against LibreOffice's own templates
gaps.md row 19 now lists what's left in ODT after the template work:
- emphasis marks and turned characters, which have no WordCraft model;
- variable fields;
- control looks;
- embedded spreadsheets' contents;
- tables wider than the margins;
- what a character style turns off.
The ROADMAP progress log has the entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 08:02:33 +00:00
Claude ce516b04f6 Gap 19: docs: ODT fidelity re-estimated at ~90%
After custom shapes, cell formulas, kept package parts, turned text,
and control sizes and buttons: ODT is ~90% in file-format-parity (20–35
h left). gaps.md row 19 lists what's left in ODT:
- emphasis marks;
- turned characters;
- conditional and variable fields;
- hidden controls and control looks;
- embedded spreadsheets' contents.
The ROADMAP progress log has the entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 23:46:56 +00:00
Claude 5f7f89c187 Gap 19: docs: ODT status in gaps, roadmap progress log
Gap 19's row now says what ODT reads and writes and what is left, and
its estimate drops from 18–27 h to 10–16 h. The roadmap's progress log
covers the ODT work since text boxes: shapes through password
protection, forms, ruby and flow breaks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 19:05:48 +00:00
Claude 2718ca1b6b Gap 19: ODT text boxes keep their text
A `draw:frame` holding a `draw:text-box` (ODF 1.3 §10.4.3) opens as a
WordCraft text box: a text box shape of the frame's size, in line or
floating as the frame is anchored and placed, whose story holds the
box's paragraphs (read as a note's are, with the paragraph and frame
around them put aside meanwhile, so a picture inside the box is a frame
of its own). The text was dropped on open.

Export writes each text box as a frame with a thin border and white fill
(`draw:fill` as well as `fo:background-color`: LibreOffice fills a frame
without `draw:fill` with its default blue). The module documentation now
lists what the reader and writer handle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 02:56:36 +00:00
Claude 488db37516 Gap 19: ODT contextual spacing and LibreOffice layout settings
Contextual spacing (`style:contextual-spacing`, ODF 1.3 §20.255: no
space between paragraphs of one style) is read from paragraph, custom
and kind styles and written back, and the List Paragraph style is
written like any other, so bulleted items keep their spacing; they were
written in Standard and spread apart.

LibreOffice switches many layout behaviours by `settings.xml`, and a file
without them is laid out its own way. A file's own `settings.xml` is now
kept through a save; a document from elsewhere gets the switches
LibreOffice uses for Word files, as WordCraft lays text out as Word
does. Tab stops are written from the margin either way. A Word-made
document saved as ODT drew 38,652 px apart from LibreOffice's own ODT of
it; now 3,302, with the same line breaks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 02:41:03 +00:00
Claude f719ff2f74 Gap 19: ODT custom character styles kept on open and save
A common text style other than LibreOffice's link, note-mark and
list-label styles (Emphasis, a user's own) opens as a document character
style with its display name, the custom character style it is based on
and its own formatting; the text in it names it, and only what the
spans' automatic styles add is direct formatting. Before, the style was
lost and its formatting copied onto the text. An id a paragraph style
already has gets "Char" added.

Export writes each custom character style the text uses, and those it is
based on, as common text styles; a span's automatic style is based on
its style and holds only the run's direct formatting. LibreOffice keeps
the style when it saves the file. Automatic styles of styles.xml are
never taken for custom styles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 02:27:03 +00:00
Claude cbc19809b6 Gap 19: ODT custom paragraph styles kept on open and save
A common paragraph style other than Standard, the paragraph kinds' and
the note texts' (LibreOffice's Text body, a user's own style) opens as a
document style with its display name, the style it is based on (a
custom parent, a kind's style or Normal) and its own formatting:
spacing, indents, line spacing, alignment, keep with next, borders,
background, tab stops and text formatting. Its paragraphs stay in it;
only what the automatic styles over it add is direct formatting. Before,
the style was lost and its formatting copied onto every paragraph.
Headers, footers, notes and comments are covered too.

Export writes each custom style the paragraphs use, and the custom
styles they are based on, as common styles named by an XML-safe form of
the style id and shown by the style's name; LibreOffice keeps them when
it saves the file. A custom style may not take a name the writer uses
for its own styles (Standard, Footnote, Endnote, the kinds'), so no
style is written twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 02:04:09 +00:00
Claude 3745441899 Gap 19: ODT equations written as formula objects
Each equation is written as an embedded formula object (ODF 1.3
§10.4.3): a frame in line with the text whose `draw:object` points at a
sub-document `Object N/` (declared in the manifest as a formula) whose
content.xml is the equation's MathML, with its linear text as an
annotation. The reader already opens LibreOffice's formula objects, so
equations round-trip. They were written as their linear text.

LibreOffice scales a formula to its frame, so the frame is sized from
the equation's structure (a box per node: fractions, scripts, radicals,
n-ary operators, delimiters, matrices) and centred on the line.
Checked in LibreOffice 24.2 with LibreOffice Math.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 01:48:43 +00:00
Claude 21849ab7c0 Gap 19: ODT floating pictures keep their place and wrapping
A frame anchored to a paragraph, character or page (`text:anchor-type`,
ODF 1.3 §19.11) opens as a floating picture: its graphic style's
`style:wrap` and `style:run-through` give the wrap (square on both
sides, left, right or the larger side; top and bottom; behind or in
front of text), `style:horizontal-pos`/`-rel` and `style:vertical-pos`/
`-rel` its alignment or, with `svg:x`/`svg:y`, its offset and what it is
relative to, and `fo:margin-*` its distance from the text. Every
picture used to open in line with the text.

Export writes floating pictures anchored the same way, placed by a
graphic style of their own; pictures in line stay `as-char`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 01:34:52 +00:00
Claude 3d70bf1bb1 Gap 19: ODT paragraph borders and background read and written
Paragraph borders (`fo:border*`, ODF 1.3 §20.183; each side's
`fo:padding*` as its distance from the text) and background
(`fo:background-color`, `transparent` as none) are read from paragraph
styles, the nearest in the chain but the kind's style and Standard, and
written back on the paragraph's style. Only an empty paragraph with a
bottom border was kept before, as a horizontal rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 01:20:06 +00:00
Claude 6a30527aa4 Gap 19: ODT heading numbering read and written
`text:outline-style` (ODF 1.3 §16.34) numbers the headings of its
levels. On open its levels (format, prefix, suffix, levels shown, start)
become one list, each level linked to its heading style, and Heading
1-6 number their paragraphs through it, as Word's numbered headings do:
LibreOffice's "1. / 1.1." headings keep their numbers (they were lost).
Levels without a number stay unnumbered.

Export writes a `text:outline-style` from the numbering of the heading
styles; levels without one show no number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 01:11:31 +00:00
Claude 384ff126c6 Gap 19: ODT table cell borders and padding read and written
Cell borders (`fo:border*`: width, style, colour; ODF 1.3 §20.183) and
padding (`fo:padding*`) are read from table-cell styles. LibreOffice
gives an edge two cells share to one of them only (the left cell's
right side `none`, the right cell's left side drawing it); on open the
line goes to both cells, as WordCraft's cells each draw their own sides.
A table whose cells all say "no line" opens without a grid.

Export writes each cell's padding (its own, its table's or Word's
default) and its borders with each shared edge once, as LibreOffice
does (left and bottom, the first row's top, the last column's right), so
lines no longer double; cells used a fixed 4 pt padding and a grid even
in tables without lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 00:52:00 +00:00
Claude 64248032b2 Gap 19: ODT heading, title, quote and preformatted styles keep their look
On open, the file's own Heading 1-6, Title, Quotations and Preformatted
Text styles (named or displayed so, their whole chain but Standard)
give WordCraft's matching styles their font, size, bold, italic,
colour, spacing, indents, alignment and keep with next: a LibreOffice
heading is bold Liberation Sans, no longer WordCraft's blue heading.
`fo:font-size` percentages are read as a share of the inherited size.

On save, those styles are written from the document's own definitions
in full, instead of fixed ones, so a WordCraft heading looks the same in
LibreOffice. The styles test document now round-trips pixel-identically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 00:34:11 +00:00
Claude e533073ceb Gap 19: ODT tab stops read and written
Tab stops (`style:tab-stops`, ODF 1.3 §17.7-17.8) open with their
alignment (left, centre, right, decimal) and leader (dot, hyphen,
underline, middle dot), and are written back on the paragraph's style;
the default tab interval (`style:tab-stop-distance`) round-trips.

LibreOffice measures tabs from the paragraph's indent when its
`TabsRelativeToIndent` setting is on (its default); opening follows the
setting, and saved files carry a settings.xml that turns it off, so
positions run from the margin as in Word and WordCraft.

The flow model carries the tabs in `Para::extra` only for flows that
ask (FlowOpts::paras).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 00:24:11 +00:00
Claude a82d35bbff Gap 19: ODT character formatting read and written
`style:text-properties` beyond bold, italic and colour now open and
save: underline styles (double, thick, dotted, dashes, wave, words only)
and colour, double strike, caps, small caps, hidden text (kept, hidden),
letter spacing, scale, text raised or lowered at full size (a position,
not superscript), outline, shadow, emboss, engrave and language.

A monospaced font keeps its own name; it was read as "code" and written
as Courier New. The default style's pair kerning (`style:letter-kerning`)
is read into the document defaults and written back, so lines break
where LibreOffice breaks them (round-trip renders of the test documents
are now pixel-identical in LibreOffice).

The flow model carries this in `Fmt::extra` only for flows that ask
(FlowOpts::chars); the other formats are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 00:16:38 +00:00
Claude e4bac37236 Gap 19: ODT lists keep their number formats, bullets and identity
Each level of a list style (ODF 1.3 §16.30-16.33) is read with its
number format (1, i, I, a, A, none), prefix and suffix, levels shown,
start value and bullet character, and written back. Each `text:list` is
a list of its own unless it continues the one before it
(`text:continue-numbering`) or the one it names (`text:continue-list`);
an item's `text:start-value` restarts from there. A list inside a note
or table cell leaves the list around it alone.

Before, consecutive lists merged into one: three LibreOffice lists
numbered 1., i. and A. opened as one decimal list 1-7.

The flow model's ListInfo carries a list number and a compact label
(prefix and suffix up to 12 bytes, so "Chapter %1" fits) only for flows
that ask for them (FlowOpts::lists); the other formats are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 00:08:10 +00:00
Claude 084d26a3c8 Gap 19: ODT comments read and written
`office:annotation` (ODF 1.3 §14.1) opens as a document comment with its
author, date, initials and text; `office:name` and
`office:annotation-end` (§14.2) give its range, across paragraphs too.
LibreOffice's resolved state (`loext:resolved`) and reply threads
(`loext:parent-name`) are kept: a reply is a comment of its parent with
no range of its own. An end with no comment is dropped, and a comment
whose end never comes covers nothing. LibreOffice's all-zero date is no
date. Before, comments were dropped on open.

Export writes each comment named, with its end where its range ends,
and its replies after it naming it; LibreOffice keeps the threads when
it saves the file again. Truncated XML no longer loses the paragraph a
note or comment sits in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-10-31 23:48:09 +00:00
Claude 1dbcf073a4 Gap 19: ODT footnotes and endnotes read and written
`text:note` (ODF 1.3 §6.3) opens as a note story built as Word's: its
paragraphs in Footnote Text or Endnote Text, the first starting with the
note's own mark, the reference in the text in the reference style, and a
`text:label` custom mark kept. The paragraph a note sits in (with its
spans, list and table cell) is put aside while the note's content is
read, and goes on after it. Notes inside notes, in deleted text or in
frames are left out. Before, notes were dropped on open.

Export writes each note with a citation numbered as LibreOffice shows
it (footnotes 1, 2, 3; endnotes i, ii, iii) or its custom mark, its
paragraphs in Footnote or Endnote styles defined in styles.xml. An
insertion open around a note ends before it and goes on after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-10-31 23:31:02 +00:00
Claude 3402e9b644 Gap 19: ODT headers and footers written; text fields read and written
Export writes each section's headers and footers into its master page
(style:header, -left for even pages, -first for a different first page;
an empty element where Word shows nothing), with their automatic styles
in styles.xml under names of their own. Word's page-edge header and
footer distances become ODF's page margin plus header/footer band
height, and import converts back, so headers sit where LibreOffice
draws them.

Text fields (ODF 1.3 §7: page number and count, date, time, document
information, bookmark references) open as WordCraft fields with their
shown text as the result, and are written back as ODF fields. Fixed
dates and fields without a Word equivalent stay text; Word fields ODF
lacks are written as their result. The flow model carries fields only
when asked (FlowOpts), so the other formats are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-10-31 23:13:26 +00:00
Claude 9436271273 Gap 19: ODT paragraph spacing, indents and line spacing
Read `fo:margin-*`, `fo:text-indent`, `fo:line-height` (percent, length,
normal), `style:line-height-at-least` and `style:line-spacing` (leading,
as a proportional approximation) from automatic and common paragraph
styles (ODF 1.3 §20.204-209, §20.225, §20.317-318), and write them back
from each paragraph's direct formatting.

The default paragraph style and Standard give the document defaults, on
ODF's initial values: an ODT paragraph without spacing has none, not
Word's 8 pt after and 1.15 lines. Export writes the document's Normal
spacing as the default paragraph style, so the copy reopens the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-10-31 22:58:55 +00:00
e6a5cd05f0 DOCX: a paragraph naming an undefined style is formatted as Normal, as in Word (#5077) (#5104)
A pStyle id the style sheet doesn't define gave an empty style chain, so the
paragraph got the document defaults alone. Word formats it as Normal: a Pandoc
footer naming an undefined Footer style was taller than Word's and pushed the
last paragraph of the page over.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-31 12:03:07 -04:00
401c6c0dc2 References: a table of contents writes its page numbers as PAGEREF fields, as Word does (#5097)
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>
2026-10-31 11:53:56 -04:00
c934977157 Keytips: Alt, F shows the Backstage pages' letters; Alt, F, P and Alt, F, V print (#4960) (#4981)
* Keytips: Alt, F shows the Backstage pages' letters; Alt, F, P and Alt, F, V print (#4960)

Keytips ended as soon as File opened the Backstage, so Alt, F, P and
Word's older Alt, F, V (Print Preview) did nothing. Alt, F now badges the
side menu with Word's letters (Home H, New N, Open O, Info I, Save S,
Save As A, Print P, Export E, Close C, Options T), and V opens the Print
page too, as it does in Word. A page picked this way acts as a click on
it does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keytips: closing the Backstage ends its page letters (#4960)

The Backstage closed by its back chevron or a mouse Save while Alt, F's
badges showed left keytips waiting for a page letter. The next key was
eaten instead of typed, and picked a page with the Backstage gone: C
closed the document and S saved it. Keytips now end with the Backstage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keytips: keep the doc comment on works_without_document; ui-parity and ROADMAP rows (#4960)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-31 10:32:05 -04:00
a2f387567d Printing: the print job's PDF leaves out link annotations, so documents with links print (#4734) (#4817)
* #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>
2026-10-31 10:26:28 -04:00
54517ace14 Word count: each Chinese and Japanese character is a word (#4547) (#4629)
count_words split only on whitespace, so unspaced Chinese or Japanese
text counted as one word per run. Word counts each Han ideograph and
kana as a word; Hangul (spaced) still counts by tokens. Fixes the status
bar, the Word Count dialog and NUMWORDS, which all use count_words.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-31 10:19:24 -04:00
aiongg e8ea394f72 docs: tidy wording 2026-10-31 04:25:41 +00:00
aiongg 55958eb0ec docs: tidy agent, contributor and asset docs 2026-10-31 04:17:23 +00:00
ReesandBrandon Thomas a57dd81dba Copy selected objects along an axis with Ctrl+Shift (#4764) (#4775)
* #4764: copy selected objects along an axis with Ctrl+Shift

* #4775: preserve aligned copies through scrolling and secondary selection

* 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>
2026-10-30 00:14:26 -04:00
ReesandBrandon Thomas a4b1682b41 Index laid-out objects during connector rerouting (#3631) (#4622)
* #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>
2026-10-30 00:13:32 -04:00
2f78615291 #4720: Chart and SmartArt text the font lacks is drawn in a fallback face, so Chinese labels no longer show boxes (#4726)
Chart and diagram text was shaped in one face, the document's body font,
so Chinese legends and axis labels showed boxes while body text was fine.
Characters the face lacks now go to a fallback face, one face per run
when one covers it all, as in body text; a bold fallback face without a
bold style is emboldened.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-30 00:12:55 -04:00
9632646c5e Bibliography: keep a source's corporate author (#4848) (#4898)
* #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>
2026-10-30 00:12:17 -04:00
ReesandBrandon Thomas 18ae8b2150 DOCX: preserve custom-property bookmark links (#4767) (#4771)
* 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>
2026-10-30 00:11:27 -04:00
9a172e2241 Table Properties: row, column and cell settings follow the selection (#4857) (#4896)
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>
2026-10-30 00:10:33 -04:00
ReesandBrandon Thomas db2b3cab99 Home › Font: step each selected run from its own size (#4903) (#4910)
* 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>
2026-10-30 00:09:39 -04:00
ReesandBrandon Thomas 74b2cbb5c9 Backspace: keep Arabic base letters when removing marks (#4757) (#4761)
* 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>
2026-10-29 23:53:06 -04:00
ReesandBrandon Thomas d39be3bab8 Avoid repeated reference scans when converting notes (#4750) (#4752)
* Fix #4750: walk note references and formatting runs together

* docs: keep the gaps.md note in the status line and revision table

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 23:52:43 -04:00
ReesandBrandon Thomas 4abb77aa70 DOCX: avoid repeated searches when writing comment IDs (#4841)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 23:50:49 -04:00
5b952b2463 RTF: keep Windows and Enhanced Metafile pictures (#4259) (#4648)
The RTF reader only recognised \pngblip and \jpegblip, so a \pict
holding \wmetafile8 (how Word writes pasted and floating pictures) or
\emfblip was dropped and the document opened without its pictures.
Keep them as wmf/emf media when the bytes are a metafile (the extension
comes from the bytes; raster bytes go through the usual image path,
anything else is dropped), sized from \picwgoal/\pichgoal and the scale,
else from \picw/\pich (0.01 mm, or pixels under \wmetafile1; zero or
negative falls back to the default). The metafile renderer already
draws them and the DOCX writer saves them as .wmf/.emf.

RTF export dropped WMF/EMF pictures as well (it re-encoded every other
format through the raster decoder): it now writes \emfblip with the raw
bytes and \wmetafile8 without the placeable header, \picw/\pich in
0.01 mm.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 23:43:55 -04:00