Commit Graph
761 Commits
Author SHA1 Message Date
Claude 6f6559aacb ODT: widow control as a profile case; tabs measured from the indent kept
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
2026-11-03 06:18:46 +00:00
Claude 48ff99e52c M5 #19: LibreOffice reads WordCraft's ODT settings; paragraph spacing setting
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
2026-11-03 00:49:05 +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 078945e10c Emphasis marks: modelled, drawn, set, and kept in DOCX and ODT
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
2026-11-02 09:46:23 +00:00
Claude 56188b97f4 Gap 19: ODT variables as SET/REF fields; SET evaluated; character styles keep what they turn off
- 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
2026-11-02 08:37:37 +00:00
Claude ab02c85701 Gap 19: ODT fixes found by round-tripping LibreOffice's own templates
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
2026-11-02 06:36:36 +00:00
Claude 7848757954 Gap 19: ODT hidden form controls kept; .ott templates opened and saved
- 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
2026-11-02 05:49:16 +00:00
Claude 72e9593de9 Gap 19: ODT index marks as XE fields; updating an index held around its entries
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
2026-11-01 19:04:44 +00:00
Claude b36e0b9fe9 Gap 19: password-protected ODT opened and saved
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
2026-11-01 14:24:03 +00:00
Claude 135ce2229d Gap 19: ODT charts in drawing groups drawn on open
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
2026-11-01 12:49:08 +00:00
Claude 3cc5185b1e Gap 19: ODT charts opened and saved as editable charts
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
2026-11-01 12:43:51 +00:00
Claude 694ef8da96 Gap 19: flat OpenDocument text (.fodt) opened and saved
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
2026-11-01 10:05:45 +00:00
Claude c6bb7a2f57 Fields: a date-time picture's quoted text is shown as it is
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
2026-11-01 09:12:52 +00: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
6dae5a1d17 Detect CSV separator from the first non-blank line (#5042)
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>
2026-10-31 10:50:10 -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
socrabs151andBrandon Thomas 028d413ad6 #3676: page parameters saturate instead of wrapping to page one (#4289)
* 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>
2026-10-31 10:08:13 -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
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
665ded2800 Cross-reference: captions in a style based on Caption (#4411) (#4881)
* #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>
2026-10-30 00:09:01 -04:00
Yasas SriandBrandon Thomas d40e0eeb47 #4086: highlighting at a caret leaves the word alone (#4701)
* #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>
2026-10-30 00:08:31 -04:00
Harrison BoothandBrandon Thomas 2e790d549c Backspace after a floating shape's anchor keeps the shape (#4592) (#4641)
* 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>
2026-10-30 00:07:53 -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
22e99fc2b7 Tables can be copied and cut: whole tables, rows and cells (#1744) (#1784)
* #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>
2026-10-29 21:47:27 -04:00
OPUZZLES 01a1bf3921 M9: assert unused recipient labels are empty (#4880) 2026-10-29 18:14:46 +01:00
OPUZZLES 5a9e414d1e M9: generate all recipient label sheets (#4880) 2026-10-29 17:49:08 +01:00
Dylan M. Taylor ab91e9f96c Deflake the IME epoch test 2026-10-28 01:06:49 +00:00
Dylan M. Taylor a2435388e5 Refactor engine for strict Clippy checks 2026-10-28 01:06:48 +00:00
aiongg 0404b4199a test: file_access scan stops only at inline test modules 2026-10-26 22:10:26 +00:00
aiongg 835704f870 refactor(engine): move session::ime_state to the crate root as ime 2026-10-26 21:16:49 +00:00
aiongg 39336fe844 refactor(engine): move session::snapshot_types to the crate root as snapshot 2026-10-26 21:16:49 +00:00
aiongg 5a311f4615 refactor(engine): move session::prefs to the crate root as prefs 2026-10-26 21:16:49 +00:00
aiongg 8f1f3c6132 refactor(engine): move session::view to the crate root as view 2026-10-26 21:16:49 +00:00
aiongg e027a4244b refactor(engine): move session::doc_state to the crate root as doc_state 2026-10-26 21:16:49 +00:00
aiongg 3c4d7adc87 refactor(engine): move session::locale to the crate root as locale 2026-10-26 21:16:49 +00:00
aiongg cfa90220d9 refactor(engine): split session.rs into session/ 2026-10-26 21:16:49 +00:00
aiongg 8f10bd1b9f refactor(engine): split cmd/table.rs into table/ 2026-10-26 21:16:49 +00:00
aiongg a82063e50a refactor(engine): split cmd/insert.rs into insert/ 2026-10-26 21:16:49 +00:00
aiongg 342de16153 refactor(engine): split cmd/text.rs into text/ 2026-10-26 21:16:49 +00:00
aiongg d66f34b774 refactor(engine): split cmd/references.rs into references/ 2026-10-26 21:16:49 +00:00
aiongg 0a6c7605f6 refactor(engine): split cmd/para.rs into para/ 2026-10-26 21:16:49 +00:00
aiongg fbc95e1e10 refactor(engine): split cmd/citations.rs into citations/ 2026-10-26 21:16:49 +00:00
aiongg fac4b550c1 refactor(engine): split cmd/mailings.rs into mailings/ 2026-10-26 21:16:49 +00:00