Commit Graph
147 Commits
Author SHA1 Message Date
2c5249d13e All Caps draws ß as SS, as Word does (#1581) (#1583)
* #1581: WIP

* #1581: All Caps and Small Caps draw ß as SS, one character for the caret

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

* #1581: exact caret tests, small-caps scaling, symbol runs never expand

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:50:23 -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
e9e8af517c Layout: a table's percentage width past 100% is kept (#1598)
* Layout: a table's percentage width past 100% is kept

Word keeps a preferred width over 100% of the text column (w:tblW
w:type="pct" above 5000). A cover-page footer table at 131% with the
left margin as a negative indent spans the page edge to edge in Word;
WordCraft capped it at 100%, so it ended three-quarters of the way across.

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

* Contributors: add Fin Warman

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:38:18 -04:00
AlikandBrandon Thomas 2a1cdbb17f Lists: Russian letter numbering draws а) б) в) instead of digits (#1577)
russianLower/russianUpper showed 1), 2), 3). They now follow Word's 29-letter sequence (no ё, й, ъ or ь; ы kept), then repeat the letter (аа, бб, ааа), as observed black-box in Word.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:37:37 -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
85aa9cdea4 #1376: a word too wide for the strip beside a floating picture moves on whole (#1439)
* #1376: a word too wide for the strip beside a floating object moves on whole

Text in the narrow strip beside a square-wrapped picture was split mid-word
("docu" | "mentazione") when the strip was wider than the 20pt minimum but
narrower than the word. A span narrowed by a floating object is now used only
if the line's first word fits it; otherwise the word tries the row's next span,
or goes below the object. A word wider than the whole line still breaks in
place, and the 20pt minimum measured for floating tables is unchanged.

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

* #1376: a line after a skipped strip starts its row; word width skips hyphenation points

A line placed in a row's second span after the first was skipped was marked as
beside a line before it, so line numbering and pagination treated it as the
second half of a row. The word measured against the full line now runs to the
next real break, not the first hyphenation point, and a row with nothing on it
jumps below an object that actually crosses the column.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:08:04 -04:00
rabis7575andClaude Opus 5.5 68d15dafba Layout: formats past 65,536 in one paragraph keep their own style (#1340) (#1421)
The index was a u16 clamped to u16::MAX while the style was still
appended, so every distinct format past the 65,536th in a paragraph
drew with the last one that fit: its font, size, link and tracked-change
marking. It is now a u32, like the glyph ranges beside it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:07:15 -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
e5780698f2 Accessibility: Windows Magnifier follows the caret onto a new line (#1119) (#1247)
Windows Magnifier keeps the text cursor in view through UI Automation: the
caret's own box has no width, so it takes the box of the character after
the caret, and when that has none either, of the two characters before it.
The canvas described a line's end mark (the paragraph mark, a line break)
with no width, so on the empty line Enter or Shift+Enter makes Magnifier
looked at the end of the line above and stayed there until two more
letters were typed.

A line's end mark now takes the room a selection shows a paragraph mark
with: PARA_MARK_WIDTH (5 pt, now shared with selection_rects) at the line's
visual end (Line::mark_box). Checked live with Windows Magnifier, its view
read with MagGetFullscreenTransform: after Enter and Shift+Enter it moves
to the new line at once.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 18:15:46 -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
9f7330664b Word forms keep their legacy form fields and rows' skipped grid columns when saved (#1184)
* Word forms keep their legacy form fields and rows' skipped grid columns when saved (part of #1174)

* Tests: field initializers name the new form field

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:58:07 -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
ReesandBrandon Thomas 248dae4bd1 fix(layout): prevent list labels from inflating line height (#601)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 17:21:20 -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
3e20488c15 Comments: an option to show only the selected text's comment (#1055) (#1066)
* test(engine): drop a needless drop() that clippy 1.97 rejects (drop_non_drop)

* Comments: a click inside a selected balloon moves the caret in its text (#1013)

- The canvas under the markup area also "contains" the pointer, so a press on the balloon counted as a click on the page and left it
- Presses over a balloon card now belong to the card, in both the deselect check and the document click in mouse()
- Test: press, drag and release inside the selected balloon; a click on the page still leaves it

* Comments: an option to show only the selected text's comment (#1055)

- Track Changes Options › Comments: "In the margin" (default, as in Word) or "Only for the selected text"
- The second mode takes comments out of the margin column; the comment under the caret, or the clicked one, floats beside the page, over its edge in a narrow window
- The floating card has its own foreground layer, so presses on it never reach the canvas
- review.trackingOptions takes commentDisplay: margin|selected
- comment_range_at_caret: the comment whose range holds the caret
- Strings in all catalogs; tests for the option, the helper and the card's clicks

* vi: comment display options

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:55:27 -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
8196624e83 Caret: Down and Up from a line start beside a table go into it, not past it (#1021)
Down from the start of the line above a table (or Up from the start of the
paragraph after it) skipped the whole table: `DocLayout::vertical` took the
nearest line covering the caret's x, and a cell's lines start a cell margin in
from the table's edge, so the paragraph beyond the table won. Lines between
the caret's paragraph and that line in document order now come first: the
caret goes into the nearest such line's table row, to the cell under it or the
horizontally nearest. Another cell of the caret's own row never counts, so Down
in a table still goes to the cell below. Pinned in `tests_typing.rs` and
`docs/typing-parity.md`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 15:31:22 -04:00
0a2b429b05 fix(pdf): inline pictures keep their alt text in tagged PDF (#1502) (#1526)
* fix(pdf): inline pictures keep their alt text in tagged PDF (#1502)

Inline pictures were tagged as Figure with no alt, so the exporter fell
back to "picture" and screen readers announced every inline image that
way, while floating pictures kept their alt text.

Draw::Image now carries the picture's alt text from the InlineObject, and
the Placed::Lines branch of the PDF exporter passes it to the Figure tag,
including for rotated inline pictures. A picture without alt text still
falls back to "picture".

Adds a regression test in crates/pdf/src/tests.rs.

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

* rustfmt

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 14:45:18 -04:00
52e56f8d76 PDF export: All Caps text extracts correctly (#1473) (#1509)
* #1473: WIP

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

* #1473: list labels and field results in caps extract as shown

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 14:27:23 -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
a124d58e5b Dark page: highlights are dimmed so the text on them stays readable (#1252) (#1498)
* #1252: dim highlights on a dark page so the text on them reads

On a dark page (View > Switch Modes) highlight and character-shading fills
were lightness-inverted like every other colour, which leaves saturated
hues such as yellow bright under white text. They are now marked as text
backgrounds (Draw::Fill text_bg) and dimmed toward black keeping their hue
(RenderOptions::ink_bg; yellow -> ~#4a4a10), and text drawn on them
(Draw::Glyphs on_text_bg) takes the lighter of its colour and its inverted
colour (RenderOptions::ink_text), so black and Automatic text draw white
with WCAG contrast >= 4.5 on all 16 highlight colours. Light page, print
and PDF are unchanged.

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

* #1252: text on a dimmed highlight always reaches 4.5:1; lines too

Glyphs and lines now carry the text background they sit on
(Draw::Glyphs/Draw::Line text_bg: Option<Rgb>) instead of a bool. On a
dark page RenderOptions::ink_text takes the lighter of the colour and its
inversion, and falls back to white when that is under 4.5:1 against the
dimmed background (pure blue on a dim olive was 1.34:1). Underlines and
strikethroughs on a highlight use the same rule. The dim constants are
crate-private and documented as assuming the default dark paper grey.

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

* #1252: docs: the 4.5:1 rule for text and lines on highlights

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:58:53 -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
dea3e217fa Shapes: rounded rectangles keep their own corner radius (#1476)
* Shapes: rounded rectangles keep their own corner radius

A roundRect's adj (its corner in 100000ths of the shorter side) is read, kept
on the shape, saved back, and used for the screen, the PDF and Tight/Through
wrapping. Without one the corner stays at 16%.

* Render: test rounded rectangle corners through layout and rendering

* Docs: progress log and file-format parity for rounded rectangle corners

* Render: pass the shape's corner to the caret-background hit test (#299 code from main)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:42:34 -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
17ec132c7c Tables: thick cell borders meet in closed corners (#1461)
A horizontal cell edge ran from cell edge to cell edge, so at a thick
vertical edge it started on that edge's centre line and left a notch half
the border width wide at every corner. Horizontal edges now run on over
half of each visible vertical edge, as Word draws them.

Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:19:51 -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
156029fe63 Layout: a list label moves with its text when the paragraph is aligned (#1159) (#1163)
* Layout: a list label moves with its text when the paragraph is aligned (#1159)

Alignment shifted the text of a line but not the list label of the first line,
so a centred or end-aligned bullet or number stayed at the margin while its text
moved away. In Word the label stays attached to the start of the text. The
first line's label now moves by the same shift (not after a tab stop, where the
text before the tab stays too). Left-to-right and right-to-left paragraphs are
covered by one test.

* Docs: update the progress docs for this change

* Docs: put the new sentence in the WordCraft / evidence column

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 12:51:19 -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
927a305943 Layout: a picture after a content control's start stays on its line (regression from #392) (#1102)
Since content controls are kept (#392), a picture content control puts a
zero-width start marker before its picture. Line breaking keeps a line's
first item even when it doesn't fit, but counted the marker as that item, so
a picture a little wider than its line wrapped onto a second line, leaving a
marker-only line above it; in a fixed-height text box the picture's line was
then cut off. A logo in a header text box (which 0.5.0 drew) disappeared.

Markers before an item no longer count, as a display equation's check already
does.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:54:07 -04:00
Marco Arnulfo aa91e46753 Shapes: the drag preview shows the shape alone, not what was behind it (#1097) 2026-10-14 01:52:59 -04:00
0f9dc5c6ae #776: draw Wingdings, Wingdings 2/3, Webdings and Symbol text as Unicode symbols (#876)
Word's symbol fonts are proprietary and never bundled, so text set in them
showed the substitute's letters. New wordcraft_doc::symbol_fonts maps every
code (stored as U+F020-U+F0FF, e.g. w:sym, or as a plain 8-bit code) to its
Unicode equivalent: Symbol from Unicode's Adobe symbol.txt, the Wingdings
and Webdings sets from Unicode L2/12-368 (WG2 N4384), checked against UCD.
Layout shapes the mapped characters (fallback faces draw them); PDF text,
copy as text and TXT export read as the symbols; the stored text and docx
round trips are unchanged.

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-12 11:26:21 -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
7f1085fbbd #856: click in the left margin (selection bar) selects lines (#879)
A click in the left margin beside body text selects that line (trailing
space, and the paragraph mark on a paragraph's last line), a double-click
the paragraph, a triple-click or Ctrl/Cmd+click the document; dragging
there selects whole lines and Shift+click extends by lines. The pointer
shows the arrow there. Layout gains selection_bar_at, body_line_at and
line_span; select.line takes an optional pos; select.dragUnit takes the
units line and document.

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-12 11:08:43 -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
Brandon ThomasandClaude Opus 5.5 681a57fc1a Rendering: kerning and automatic paragraph spacing like Word (#923)
Found by rendering 12 US government Word documents (NIH, EPA, MCC) in
Word for Windows and WordCraft and comparing the pages side by side,
then pinning each rule down with synthetic documents measured in Word.

- Kerning: the shaper kerned every run, so WordCraft's lines held more
  text than Word's and broke differently. Word kerns only a run with
  Kerning for fonts (w:kern) at or above that size; no w:kern and
  w:kern="0" are unkerned. New documents keep kerning from 1 pt, like
  Word's Normal template.
- Automatic paragraph spacing (w:beforeAutospacing/w:afterAutospacing,
  common in text pasted from web pages) was ignored. It is 14 pt at
  any size, none before the document's or a cell's first paragraph,
  none after a cell's last, and the larger of two neighbouring spaces
  counts. Read, written back, and settable through para.spacing
  (beforeAuto/afterAuto); a number, Ctrl+0 and Remove Space After
  replace it as in Word.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 10:18:05 -04:00
Brandon ThomasandClaude Opus 5.5 3ede32e80e Space before at the top of a page like Word (#932)
* Layout: space before at the top of a page after page breaks, as in Word

Found comparing Microsoft's specification documents with Word for
Windows, then measured with 32 synthetic documents in compatibility
modes 12 (Word 2007) and 15 (Word 2013+):

- A paragraph that ends with a page break keeps its mark with the
  break: WordCraft put an empty line at the top of the next page.
- The paragraph after it, first on the new page, gets no space before.
- A paragraph that starts with a page break starts on the new page
  with its space before there (WordCraft put the space on the old page).
- A page break before keeps the space before in modes up to 14 and
  drops it in mode 15.
- Natural breaks, section breaks and the first page were already right.

All 32 cases now land where Word puts them (within 0.3 pt), and the
caret still reaches a paragraph mark kept with its break.

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

* Layout: after a page break before or a section break, the space after above counts

Measured in Word for Windows (10 more cases): at the top of a page after a page break before
(modes up to 14) or a next-page section break, the previous paragraph's space after comes off the
space before (24 pt before after 10 pt after leaves 14 pt; after 30 pt leaves none). This put the
numbered Heading 1s of Microsoft's specification documents 9 pt too low. All 42 page-top cases now
match Word.

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:12:54 -04:00
Brandon ThomasandClaude Opus 5.5 97ade895d7 Layout: text after a column break takes its new column's width (#934)
Found in NIH's SARP form compared with Word for Windows: the form's
second section has a narrow label column (67 pt) and a wide text column
(438 pt), and each answer starts with a column break into the wide one.

Before: the paragraph's lines were broken once, for the column it
started in, so the text after the break ran a word or two wide down the
wide column. After: when a column break lands the rest of a paragraph
in a column of another width, that rest is broken again for it, as in
Word. Equal columns are unaffected (nothing is laid out twice).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 10:09:42 -04:00
Brandon ThomasandClaude Opus 5.5 672522b2ed R22: paragraph-relative floats start above the space before (#941)
Word positions a floating object "relative to paragraph" (and to its line)
from the paragraph's start above its space before, below the previous
paragraph's space after. WordCraft measured from the first line, so floats
in spaced paragraphs sat lower by the space before. Measured in Word for
Windows: 10 synthetic cases.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 10:05:48 -04:00
Brandon ThomasandClaude Opus 5.5 5383d80f75 An empty continuous section mark takes no room (#940)
* R21: an empty continuous section mark takes no room

An empty paragraph that only carries a section break before a continuous
section takes no room in Word: the text after it follows right on, and
columns balanced above it don't count it. A break on a paragraph with text
keeps its line. Measured in Word for Windows: 10 synthetic cases. The SARP
form's fields drifted a line lower at each of its continuous breaks.

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:02:07 -04:00
Brandon ThomasandClaude Opus 5.5 fc52079f58 R19: keep with next splits a paragraph that doesn't fit anyway (#933)
Word moves a "keep with next" paragraph to the next page only when it would
fit on this one by itself; one that doesn't fit anyway breaks as usual, its
last lines going over with the next paragraph (with keep lines together it
moves whole). A chain of kept paragraphs moves together. Measured in Word
for Windows: 20 synthetic cases. NIH's five-line kept paragraph no longer
leaves a third of a page empty.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 09:58:26 -04:00
Brandon ThomasandClaude Opus 5.5 7d37b14679 R16: list indents and style indents combine like Word (#927)
A numbered paragraph style's own indents (and those of styles based on it)
now beat the list level's, its base styles' don't, and the paragraph's own
list beats every style. A paragraph that switches its list off (numId 0)
drops the styles' indents, as Word does; the first Backspace in a List
Paragraph item makes the item's indent the paragraph's own so the text stays
put. Measured in Word for Windows: 27 synthetic cases, all match. NIH's
Heading 3s sit at their 0.9" and its "sample text" paragraphs at the margin.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 09:55:41 -04:00
Brandon ThomasandClaude Opus 5.5 5aa8151a53 Text frames in headers, footers and cells, kept on save (#935)
Found in NIH's monitoring-plan template compared with Word for Windows:
its footer puts the page number in a text frame (w:framePr: centred,
text wrapping around it), and Word draws "NCCIH DSMP Template" ending
just left of the number on the same line.

Before: frames other than drop caps were ignored on import, so the
number took a line of its own above the footer text (making every page
a line shorter), and the frame was lost on save.

After: a paragraph's text frame is read (size, anchors, alignment,
offsets, wrap, distances) and written back. In headers, footers, table
cells and text boxes it is placed like a floating object, sized to its
text unless it has a width, and the text after it wraps around it; the
DSMP footer now matches Word's. Frames in the body text still lay out
in line.

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