Commit Graph
115 Commits
Author SHA1 Message Date
dfgodvananddfgodvan 99ff497180 Layout: a roman page number in the footer stays roman when the body shows the page count (#2966)
A paragraph with a PAGE field is cached by the page number, the totals, the
section and the bookmarks, but not by the number format the field shows the
page number in. Paginating lays the footer out to know where the body ends,
before the section's number format is set in the field context, so as
decimal. When the body shows NUMPAGES or SECTIONPAGES, the pass that has the
right totals made that layout's key equal to the page's own footer's, and the
cached decimal one was drawn: page 2 of a section numbered in lower-case
roman showed "2", not "ii". The number format is now part of the key.

Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
2026-10-18 16:55:08 -04:00
9b9eeb8c61 Fit inline text boxes with Resize shape to fit text to their text (#2912)
Fixes #2791

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:52:32 -04:00
GavinQiErandBrandon Thomas fe4faca3f0 fix: do not leave a dragged object on its page (#2720)
While a floating picture, shape or text box is dragged, the page raster is
not repainted (the texture cache key does not include the drag state) and a
translucent preview is pasted at the pointer. The user therefore sees the
original copy at full opacity plus the preview, i.e. two copies of the same
object.

Exclude only the dragged object from that page's display pass, and repaint
that page once when the drag starts and once when it ends; the preview keeps
using the previous texture. The rest of the page is left untouched (no
full-page clear, no per-frame repaint).

Adds three tests: a dragged inline object and a dragged floating shape are
left out of their page, and the view key changes when a drag takes an object
off the page.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:49:30 -04:00
Fin WarmanandBrandon Thomas 596176c794 371: clear header tables for floating body tables (#2660)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:49:00 -04:00
Fin WarmanandBrandon Thomas 5909ab642a T372: wrap body around page-relative footer tables (#2659)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:47:30 -04:00
Fin WarmanandBrandon Thomas 60617d9a71 371: count shared section pages in fields (#2654)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:47:03 -04:00
Fin WarmanandBrandon Thomas 473df388f3 Keep the line in an empty section starting a page (#2609)
* 371: retain the line in an empty page section

* 371: resolve hidden section marks

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:44:45 -04:00
5fdd0a0e89 Watermarks in header table cells drawn behind the text (#1775) (#2580)
A floating picture or VML shape in a header table cell is placed on the page behind the text.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:43:55 -04:00
Fin WarmanandBrandon Thomas b5759bcd99 351: reserve footer flow-table border clearance (#2585)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:42:54 -04:00
Fin WarmanandBrandon Thomas 5b211d6dc5 345: ignore explicit page breaks in table cells (#2574)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:42:39 -04:00
Antoine BoxhoandBrandon Thomas 61a626820e #1552: no spelling squiggle under the word being typed (#2183)
Word draws no red squiggle under the word being typed. It appears when the word ends or the
caret leaves it by a key or a click. The session records the word after a text command that
edits the caret's paragraph, with apostrophes and hyphens counted as the spelling check counts
them, and drops it once the caret is anywhere else. The layout leaves that one range out of the
spelling squiggles, and a caret move without an edit lays out only that paragraph again. F7 and
the Spelling pane still report the word, and grammar marks are unchanged.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:39:21 -04:00
Fin WarmanandBrandon Thomas d7bd5bcd3c Layout: preserve additive spacing after section breaks (task 243) (#2433)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 10:24:41 -04:00
Marco ArnulfoandBrandon Thomas 8f0000de63 Text boxes: framed paragraphs run with the box's text, not drawn over each other (part of #2357) (#2383)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 10:23:27 -04:00
d6f20f93fd Tables: at-least rows taller than a page start on a fresh page; header rows follow (#1742) (#2004)
* WIP for #1742

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

* #1742: parity doc separates the Word-observed case from synthetic-test behaviours

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:57:47 -04:00
Fin WarmanandBrandon Thomas c92d120098 Honor table row keep-next during pagination (#2494)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 04:57:07 -04:00
Marco ArnulfoandBrandon Thomas 61ad110e9f Layout: automatic space between Asian text and Latin text or digits (autoSpaceDE, autoSpaceDN) (#2358) (#2397)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 04:54:39 -04:00
c5a9c954f7 Track Changes Options: Moves (move tracking, Moved from/to marks and colours) (gap 5) (#1779)
* track-move-options: WIP

* track-move-options: Moves options in Track Changes Options and w:doNotTrackMoves

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* track-move-options: Moved from Hidden leaves the layout; per-revision hiding

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* track-move-options: Word's mark lists, (none) moves are plain, no borders on collapsed paragraphs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* track-move-options: hr and sv translations for Moved from:/Moved to:

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 04:24:34 -04:00
13070ca85e Layout: field instructions do not add blank paragraphs (#1665)
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 21:59:41 -04:00
daf8caada3 Tables: repeated header rows move to the next page with the first row (#1704) (#1705)
A table with repeated header rows that started near the bottom of a page left the header rows
there alone when the first row after them didn't fit, then repeated them on the next page above
that row. Word starts such a table on the next page.

place_table now checks, before the first row, whether the row after the header rows can start
under them (whole, or split when it may split). If it can't and the page already holds
something, the table starts on the next page. A table at the top of a page keeps its header
there as before.

Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:31:10 -04:00
Fin WarmanandBrandon Thomas faa621104c Layout: empty line breaks do not move floating tables (#1658)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 21:22:19 -04:00
6eeb6d10db A floating object anchored after a page break stays on the page its paragraph starts on (#1632)
Word's cover pages are one paragraph holding a page break and then the
page-anchored text box with the title and the approval table. Word
draws a floating object on the page its anchor paragraph starts on;
WordCraft drew it with the line holding the anchor, which after the
break is the first line of page two, so the cover was blank and the
title box sat over the body text of page two.

`place_objects` now places a paragraph's floating objects from its first
piece, whichever of its lines holds the anchor, and none from later
pieces; inline objects still go with their own line. The wrap areas were
already registered from the paragraph's first page, so nothing changes
for text flowing around the object.

Co-authored-by: Daniele Mori <Dinasty@hotmail.it>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 21:03:41 -04:00
fe9d560b49 Layout: paragraph spacing adds up when HTML auto spacing is off (#1612)
space after and the second's space before, as Word does by default. Word
adds them instead when the document's compatibility options include
w:doNotUseHTMLParagraphAutoSpacing. Measured in Word with five paragraphs
of 12 pt before and after: 24 pt between them with the option, 12 pt
without, the same in compatibility modes 14 and 15. A 60-page report with
the option lost 6 pt per paragraph, about 60 pt on its cover.

The option is read, laid out in the body and in boxes, and saved back.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:29:43 -04:00
f3f110b442 Layout: an inherited footer sizes its percentage tables by its own section (#1620)
A header or footer a section inherits (Same as Previous) keeps the
percentage widths of its tables relative to the section that defines it.
Measured in Word: a 100%-wide footer table defined by a portrait section
stays 451 pt wide, centred, on landscape pages that inherit it, and spans
the landscape width only when that section names the footer itself.
Paragraph alignment in the same footer follows the landscape page.

Found on a report whose landscape calculation pages drew their footer
table full width where Word draws it at the portrait width.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:25:49 -04:00
6d5cc9ef84 Layout: an empty section mark stays on a full page (#1614)
* Layout: an empty section mark stays on a full page

An empty paragraph that carries a section break, when the next section
starts on a new page, needs no room of its own in Word. Measured: 50 lines
filling a page, then that paragraph, then the next section makes two pages
in Word; WordCraft made three, the middle one blank. A plain empty
paragraph in the same place still moves on, in both.

Found after a landscape drawing page in a 60-page report.

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

* Layout: drop an unneeded mut in the section mark test

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-14 20:23:15 -04:00
1b60773196 Tables: a row with less room than its minimum height moves on whole (#1618)
* Tables: a row with less room than its minimum height moves on whole

Measured in Word with about 49 pt left on a page: a table row of eight
lines splits there, but the same row with an at-least height of 63.6 pt
goes to the next page whole, and so does one holding a nested table.
Without the height both split, as before. WordCraft split them all, so a
report's title row (a logo beside a small nested table) started at the
foot of its cover page.

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

* Tables: preserve splitting for oversized minimum-height rows

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:19:27 -04:00
fee9b984eb Tables: text running past an exact-height row is cut off (#1617)
* Tables: text running past an exact-height row is cut off

A row with an exact height (w:trHeight w:hRule="exact") keeps that
height whatever its cells hold. Word cuts the text that doesn't fit at
the cell's edge; WordCraft drew it over the rows below, so a certificate's
identification block came out as overlapping lines.

The cell is marked as clipped in layout (Placed::Cell clip), and display
draws its lines inside that cell's clip, on screen and in PDF. Hit
testing is unchanged: the lines stay where they are in the layout.

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

* Tables: keep row clipping compatible with Rust 1.90

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:15:39 -04:00
241743b4ab Layout: a bookmark after a page break takes no line (#1615)
Word writes the next heading's TOC bookmark start into the paragraph that
ends with a page break. The line after the break then held that marker,
so it wasn't the empty mark kept with the break (#932): it went onto the
next page with the paragraph's spacing and pushed the heading 17 pt down.
Lines holding only zero-width markers (InlineObject::is_marker) now count
as empty there.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:08:02 -04:00
c2872f23c3 Layout: a page break followed by another page start makes one page (#1611)
A paragraph holding only a page break keeps its mark on the page before
(#932), but its space after still went onto the new page, so a heading
with page break before no longer stood at the top and broke again: a
blank page. The space after now stays with the mark. A next-page section
break carried by such a paragraph also started a second page; the empty
page the break started is now used instead.

Found on a 60-page inspection report: two blank pages Word doesn't have.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:04:14 -04:00
ba1e28382d Layout: page numbers can restart at 0 (#1601)
w:pgNumType w:start="0" was read but laid out as 1: the page counter
was set to one less than the restart and could not go below zero. A
report numbering its cover 0 read "Page 2" on the page Word calls 1.
The restart is now the next page's number itself.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:38:36 -04:00
8fd4951d76 Layout: widow control moves a three-line paragraph whole (#1575)
With room for two of its three lines at the bottom of a page, a paragraph was
split two and one, leaving a widow; taking a line along would leave an orphan,
so Word moves the paragraph whole. A paragraph continued from the page before
may still take one line along from two (no orphan there).

Co-authored-by: przemek-arch <263159043+przemek-arch@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:37:18 -04:00
9c57b950b9 Page Setup: Justified vertical alignment (#1320) (#1435)
* #1320: justified vertical page alignment, in DOCX, layout.verticalAlign and Page Setup

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

* #1320: a section's last page takes its own vertical alignment; pin space-after and multi-column cases

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

* Docs after rebasing on main

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

---------

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-14 19:31:37 -04:00
e0a1346590 Layout: inside/outside floats swap sides on even pages; Page Setup shows mirror margins (#1181) (#1239)
* Layout: inside/outside floats swap sides on even pages; Page Setup shows mirror margins (#1181)

Floats aligned inside or outside, or placed relative to the inside or
outside margin, were always put as on an odd page, so shapes Word puts
in a book's outside margin all came out on the right. They now follow
the page they're on (ECMA-376 ST_AlignH, ST_RelFromH): on an even page
the inside is the right side. Floating tables (w:tblpXSpec) too.

With mirrored margins, even pages are laid out as odd ones and then
shifted; float positions now allow for that shift, so floats placed
from the page edge stay put (the "known limit" in #452) and the left
and right margin areas are each page's own.

Mirrored margins read from a file were invisible in the UI: the Margins
menu ticks Mirrored when they're on, and Page Setup gets a Mirror
margins check box (labelling the margins Inside and Outside), applied
through a new `mirror` param of layout.margins.

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

* i18n: Mirror margins in every catalog

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

---------

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:57:44 -04:00
Jeremy YaoandClaude Fable 5.1 c3bdcd73b4 Harden against hostile-document DoS and non-web link export (#1235)
* Harden against hostile-document DoS and non-web link export

A security pass found several small files that make the app allocate or
compute without bound when opened (GUI, wordcraft-cli, or the MCP server),
and link targets leaving on export without the scheme check the HTML and
Markdown writers already apply. None lets a document run code or read files;
all are denial-of-service or credential-leak-on-click. Each fix bounds an
attacker-controlled size and lands with a regression test, per the
never-crash standard.

- layout: a table repeats its header rows on every page it spills onto, so
  many tall header rows paginate to O(headers x pages) placed items. Cap the
  total header-row repeats per table (lib.rs). 2 KB docx: 1.1 GB -> 124 MB.
- formats/model, html: an HTML table of many <tr> with large rowspan expanded
  to rows x 64 covered cells, and `model::table` pre-allocated a full
  rows x cols grid only to clear it. Cap rows at MAX_ROWS before
  insert_covered, bound its total work, and build the grid template with one
  row. 640 KB html: >1.5 GB -> 214 MB (a plain large table is faster too).
- docx/read/freeform: a custom-geometry Bezier segment took any number of
  points, making de Casteljau flattening quadratic. Cap points per segment.
  3 KB docx: 12 s -> 0.15 s.
- docbin: footnote/header/text-box story tables and the piece table can name
  the same backing text many times. Hold the total characters materialised to
  twice the WordDocument stream size (a ceiling no real file reaches, since
  each char costs >=1 stream byte). 45 KB doc: >1.2 GB -> 14 MB.
- render, pdf, formats/rtf: document pictures were decoded with no dimension
  limit, so a tiny file declaring a huge image could allocate before the size
  check. Decode through image::Limits with a side cap first.
- doc/link, formats/rtf, odt, latex: route exported hyperlink targets through
  is_safe_href (as HTML/Markdown already do), and extend it to reject
  protocol-relative (//host) and UNC (\host) references. Keeps file:,
  javascript:, ms-msdt: and UNC targets from being written into exported RTF,
  ODT, LaTeX and PDF where a reader would follow them (UNC leaks NTLM on
  Windows).

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

* Align picture-decode limits with the bounds already in force

Review of the previous commit: `image::load_from_memory` and
`load_from_memory_with_format` already applied `Limits::default()`, whose
512 MB allocation cap bounded every picture decode. The PDF change had set
`max_alloc = None`, loosening that; restore the default. The render and RTF
side caps (16 384 px) newly rejected wide-but-short pictures that fit in
memory, so render now uses the pixmap's own `u16` side limit (what
`premul_pixmap` enforced after decoding, applied before it instead) and the
RTF re-encode keeps only the explicit allocation cap. Net effect: rejection
moves to header time with no change to the sizes accepted. Also fix a
comment: the header-row repeat cap is per table, not per document.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-14 18:00:35 -04:00
b3af58602b STYLEREF review follow-ups: headers stay cached, switch labels in our own words (#1286) (#1294)
* #1286: key STYLEREF header caches on their per-page results; STYLEREF N finds a localized heading N

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

* #1286: reword the STYLEREF switch labels in our own words, in every catalog

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

* #1286: drop a stray table row from the ROADMAP progress log

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

* #1286: layout-parity STYLEREF row (carried over the rebase)

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

* vi: STYLEREF switch labels as reworded

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

---------

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-14 17:22:36 -04:00
ReesandBrandon Thomas bfacaf3627 fix(layout): preserve legacy table paragraph spacing (#577)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 17:22:17 -04:00
cf9a4ff7fa STYLEREF fields show each page's result in headers and footers (#775) (#882)
* #775: STYLEREF fields in headers and footers show each page's result

Running headers/footers with STYLEREF (e.g. the current chapter title) are
evaluated per page at layout time (ECMA-376 §17.16.5.59): the page's first
paragraph of the style (\l: last), else the nearest before, else after.
Style names/ids, heading levels (STYLEREF 1), character styles, and the
\n \r \w \s \t \p switches. Body STYLEREF fields update with the other
fields. STYLEREF added to Insert › Quick Parts › Field with its options.

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

* #775: STYLEREF error text in our own words

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

* Review fixes for #882: translate the STYLEREF Field dialog strings

Adds the style picker label, the six STYLEREF switch labels and the
STYLEREF description to all 18 interface catalogs.

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

* STYLEREF: reuse the style index while the body and styles are unchanged

Headers with STYLEREF rebuilt and hashed the body's style index on every
layout pass (echelon's follow-up note on #882). The layout cache now keeps
the index with the body blocks it came from (shared, so an edited block is
a different pointer) and the styles/numbering hash, and rebuilds it only
when either changed.

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

* vi: STYLEREF field strings

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

---------

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-14 16:56:47 -04:00
ReesandBrandon Thomas 8e0f42e4bc fix(layout): match Word's contextual paragraph spacing (#568)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 16:56:29 -04:00
Sazid Al Bayazid 2fd3b556f3 Layout: keep with next moves the heading when the next paragraph moves for widow control (#966) (#1049) 2026-10-14 15:48:32 -04:00
3d2002b699 Layout: a paragraph continued into the next column wraps around a shape there (#1374) (#1505)
* #1374: a paragraph continued into the next column or page wraps around the shapes there

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

* #1374: wrap relays keep equation numbers, need the same line top, skip far-off shapes

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:06:29 -04:00
0c7b4a72f2 Layout: long tables keep one layout per cell and relayout without re-measuring (#1492)
- Table autofit measured every cell paragraph at an unbounded width through
  the paragraph cache, so a long table held two layouts per cell. The cache
  now keeps only the measured narrowest/widest widths (`LayoutCache::measures`,
  same key and eviction as the layouts).
- The cache key formatted each cell's table-style text formatting with Debug
  for every cell paragraph on every pass; the hash is now memoised per pass.
- Update Fields added the hidden ListEntry style even without a generated
  list, which changed the layout environment and dropped the whole cache.
  The style is now added only when a list is rebuilt.

Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:06:17 -04:00
cyril265andBrandon Thomas 1be06ca323 Fields: PAGEREF in headers and footers shows its bookmark's page (#1477)
* Fields: PAGEREF in headers and footers shows its bookmark's page

Headers and footers are laid out once the body is paginated, so a page
reference there now shows the number of the page its bookmark starts on, in
that page's number format, instead of the result stored in the file. Like the
page totals in headers and footers, it is filled in after pagination and
doesn't make the body lay out again. Mail-merged letters use this for
"Page X of Y" per letter (Y = PAGEREF to a bookmark at the letter's end). Page
references in the body keep their result until fields are updated.

* Contributors: add cyril265

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 13:42:53 -04:00
b8b8ff6128 Layout: PAGE fields on the later pages of a paragraph show their own page (#1352) (#1481)
* #1352: PAGE fields on the pages a paragraph runs onto show that page's number

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

* #1352: keep the page map local; assert no overflow

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

* Layout: re-laying out a paragraph keeps its equation numbers

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

---------

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-14 13:25:58 -04:00
0128e117b3 Layout: table rows share their cell margins and split across pages like Word (#1177)
- Every cell in a row gets the row's largest top and bottom cell margin, so
  a cell without its own margins lines up with its neighbours.
- An exact row height covers the top border, top margin and text; the
  bottom margin comes on top. Dragging a row border, or drawing a line
  through an exact row, accounts for it.
- A row split across pages must keep its cells' bottom margin, border and
  last paragraph's space after on the first page, and widow/orphan control
  applies to the paragraphs in its cells. A row whose text fits but whose
  margin doesn't no longer moves to the next page whole.
- Each cell of a split row goes on at the top of the continuation (one
  cell's widow line no longer pushes its neighbours down), and the
  continuation is as tall as the cell needing the most room, and at least
  the rest of the row (a minimum row height holds over the pages).
- A nested table's cell the page bottom cuts through goes on from the cut
  with what is left of it, and the text after it stays below it.
- A row of an exact height never splits: it moves to the next page whole.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 13:06:24 -04:00
37193a14ba Layout: a paragraph ending with a page break leaves its space after on the old page (#1168)
A paragraph whose last line holds the page break keeps its mark on the old
page, but its space after was still added after the break, so the next page
started that much too low (and the next paragraph lost its "no space before
after a page-break mark" treatment). Word adds none of it there, in
compatibility modes 12, 14 and 15 alike.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 13:05:38 -04:00
44086d08c8 Double-click the margin to create and edit a header or footer (#960) (#1144)
* #960: double-click the margin to create and edit a header or footer

Double-clicking the top or bottom margin in Print Layout now edits the
header/footer that page shows, creating it (first page, even page or
default slot) when the page has none. insert.editHeader/editFooter take
{"page"}; insert.closeHeader takes away a created one left empty.

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

* #960: making a header by double-click is an undoable, protected edit

insert.editHeader/editFooter stay pure and hand creation to the new
mutating insert.createHeader/createFooter; closeHeader removes empty new
ones through insert.removeEmptyHeaders. An empty new header is never
saved, one an undo brings back goes on the next close, and a new header
goes in the first section (linked to later ones, as in Word).

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

---------

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-14 12:49:37 -04:00
85a3a53a8e Keep a paragraph's space after clear of the footnote separator (#1116) (#1140)
* #1116: keep a paragraph's space after clear of the footnote separator

On a page with footnotes (already there or brought by the line), a
paragraph's last line only fits if its space after fits above the notes
area too; otherwise it goes to the next page with its note (and, with
widow control, one more line). Contextual spacing and automatic spacing
between list items drop the requirement; pages without notes are
unchanged (space after may still run past the bottom margin).

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

* #1116: space after vs footnote separator uses the spacing code's tests, only on pages with notes

Contextual spacing drops a paragraph's space after only when that paragraph
asks for it, and the next paragraph counts as a list item only when it gets
a label (Numbering::shows_label), as in the spacing between paragraphs.
The kept space after is worked out only for a last line on a page with notes.

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

---------

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-14 02:03:30 -04:00
d6723153cc Drawings: VML text boxes keep their margins, fit and alignment; hidden drawings are kept but not shown (#1118)
* Text boxes: VML text boxes keep their internal margins, fit to their text and alignment

A VML text box's inset ("l,t,r,b", ECMA-376 Part 4 §19.1.2.22) was ignored,
so its text sat Word's default 7.2 pt in and 3.6 pt down even where the file
says 0,0,0,0, which is how letterheads and form headers place address blocks
exactly. It's now read like DrawingML's lIns/tIns/rIns/bIns: each value left
out keeps the default, a bare number is in EMUs, o:insetmode="auto" leaves
the defaults. mso-fit-shape-to-text (VML's "Resize shape to fit text") is read
as auto-fit, like a:spAutoFit, and the shape's v-text-anchor as the text's
vertical alignment. In a group, a member's margins go with that member's
text.

The reader feeds layout, so the editor, PDF and page images all change.

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

* Drawings: hidden shapes and pictures are kept but not shown

A drawing marked hidden (VML visibility:hidden, floating or inline;
DrawingML wp:docPr/@hidden, or a group member's cNvPr/@hidden) was drawn,
took room and wrapped text. Word for Windows shows it in no view; it's now
kept and saved hidden, but left out of layout: not placed, no wrap area, no
room in its line, nothing drawn, its text box's notes not placed. Exports to
other formats and copies leave it out, the accessibility checker no longer
asks for its alt text, and the selection pane says it's hidden.

Word for Mac draws hidden VML shapes; Windows follows the markup.

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

---------

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 02:01:09 -04:00
be7474566b Layout: blank pages put sections on the right side, and paragraph spacing is the larger of after and before (#733, rebased) (#964)
Rebased #733 (paragraph spacing larger of after and before; blank pages for odd/even section starts) onto main.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 11:18:57 -04:00
8c8d5611a4 Rulers: table column and row markers; indents in a cell measured from it (#710)
* Rulers: table column and row markers; indents in a cell measured from it (#700)

With the caret in a table, the rulers show the table, as Word's do:

- The horizontal ruler marks the column borders of the caret's row (past
  the table's left edge); the vertical ruler marks the bottom of each of
  the table's rows on the page. Each marker has the resize pointer, a
  tooltip and a name for screen readers.
- Dragging a marker drags that border exactly like a drag on the page
  (#600): a dashed guide, Escape cancels, one undo step on release
  through `table.resizeDrag`. On the horizontal ruler only the column
  left of the border changes and the columns right of it move along, so
  the table's width changes (Word's ruler); a row marker sets the row's
  height. The markers follow any drag of the table's borders, on the
  ruler or on the page.
- In a cell, the indent markers and tabs are measured from the cell's
  text area (its box less its margins); they were placed as if the
  paragraph started at the page's margin.

- Layout: `cell_margins` (a cell's own margins, else the table's, else
  Word's), so the ruler uses what the layout does.
- UI: crates/ui-egui/src/table_ruler.rs; table_resize.rs starts a drag
  from a ruler marker (`grab`) and tells the ruler where a dragged border
  is (`moved`), sharing that computation with the dashed guide.
- Tests: layout (cell_margins matches where each cell's text starts);
  kittest with real pointer events: a column marker drag (the marker
  follows, one undo step, the columns right of it keep their widths), a
  row marker drag, and an 18 pt drag of the left-indent marker in a cell
  indenting the paragraph 18 pt.

Progress docs updated: ui-parity (rulers 70% -> 85%), gaps (the ruler's
column markers are off the list), target-app-parity, ROADMAP. No new
commands or labels (the tooltips reuse "Table Column Width" and "Table
Row Height"). `cargo xtask ci` passes.

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

* Rulers: Shift works the other way round on the ruler; row markers beside a vertical merge

Checked against Word (2019, Windows): on the ruler a column marker drag
changes only the column on its left and the columns right of it move
along (as before), and with Shift the neighbour makes up the difference,
so the table keeps its width; on the page Shift does the opposite. The
ruler now follows that: `ResizeDrag::plan` takes the Shift key and flips
it for a drag started on a ruler marker.

Since #669 a vertically merged cell's box spans all its rows, so a row
marker took the merge's bottom when the merge was the row's leftmost cell.
The marker now sits on the row's shortest box. (`Placed::Cell` patterns
take `..` for #669's new field.)

- Test (kittest, real pointer events): a Shift drag of a column marker
  on the ruler gives the neighbour's width to the column on the left and
  keeps the table's width.

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

---------

Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 10:38:17 -04:00
Brandon ThomasandClaude Opus 5.5 d863811511 No automatic spacing between list items (#924)
* R17: no automatic paragraph spacing between list items

Word drops automatic (HTML-style) space before and after between two list
items, of one list or two, at any level; the first item keeps its space
before and the last its space after, and measured spacing stays. Measured in
Word for Windows: 16 synthetic cases. NIH's bulleted IND report items now
touch as in Word.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ROADMAP: keep the progress log separator under its header

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 10:22:53 -04:00