* fix(docbin): handle default codepage, VT_I4, multiple property sets, and FILETIME dates
* cargo fmt
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>
* fix(formats): support none and dual scripts in mmultiscripts, mpadded phantoms, menclose borders, and mtable column align
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Collapse the nested if (clippy collapsible_if)
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>
* Field results (STYLEREF, REF, PAGE text, note marks) shown as one atomic
cluster now place their inner clusters in UAX #9 visual order, in the
direction of the text around the field, so "פרק א" in a header no longer
draws reversed. Smart quotes already matched Word's logical-order result
in Hebrew (” in acronyms, not gershayim); pinned with a typing test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1670: fix round 1: field results shaped per bidi run, so brackets mirror correctly
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>
* WIP for #1543
* DOCX: split mixed-direction runs and mark right-to-left pieces w:rtl so Word orders them as WordCraft does (#1543)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* DOCX: right-to-left pieces of split runs say their language (w:lang w:bidi) (#1543)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* DOCX: no phantom tracked formatting on split runs; typed text doesn't inherit run direction (#1543)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Engine: text typed or inserted anywhere takes its direction from its own letters, not the caret's run (#1543)
Co-Authored-By: Claude Opus 5.5 (1M context) <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>
* WIP for #1480
* Web: an input method's DeleteSurrounding erases around the caret, so non-Latin typing no longer repeats (#1480)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Web: input-method rewrites keep formatting and the typing undo group; AutoCorrect resets the web text field (#1480)
Co-Authored-By: Claude Opus 5.5 (1M context) <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>
* #1706: WIP
* #1706: Update Fields refreshes fields in headers, footers and text boxes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
# ROADMAP.md
# Conflicts:
# ROADMAP.md
* #1706: review fixes
Revision-history row, update_fields doc comment, test covers body field and locked header field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1706: update the fields of the part being edited, as in Word
Word check: F9 with the caret in the body leaves header fields alone; with the caret in a header it updates that header. SEQ, citations and TOC run only from the body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1706: printing updates every field in the document
references.updateFields takes {"all"?: bool}: the body (with SEQ, citations, TOC) plus every part story, wherever the caret is. Printing passes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1706: ROADMAP status line after rebase
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1706: tests build the field form value added on main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1706: the test's fields take main's new dirty flag
Co-Authored-By: Claude Opus 5.5 (1M context) <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>
* 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>
* ink-arrows-2: WIP
* ink-arrows-2: Ink to Shape makes two-stroke arrows and closed-triangle heads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ink-arrows-2: review fixes — recognise each stroke once, drawing-order pairing, arrow rect
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): citation style names, audit coverage and attribution (#750)
Follow-up to #871.
- pl.tsv: add the 12 citation style names (APA, Chicago, GOST, ISO 690,
...) that hu.tsv and bg.tsv already translate.
- tests.rs: Polish also checked against the citation styles; Italian and
Hungarian may differ only by the obsolete "Style: APA".
- audit.rs: Polish joins the catalogs that must cover every looked-up string.
- ATTRIBUTION.md: credit Aleksander Kutycki (@olekkut) with Gemini 3.8 Flash for pl.tsv.
- contributors/people.toml: add [people.olekkut] entry.
Co-Authored-By: Gemini 3.8 Flash <noreply@google.com>
* i18n(pl): one-line equation gallery pieces (Large, Limit and, Log) so Polish matches Italian
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Gemini 3.8 Flash <noreply@google.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* DOCX: read every VML shape in a `w:pict`, and a `v:group`'s members (#1643)
`read_vml` took `pict.els().find(..)`, the first `v:*` child that is not a
`v:shapetype`, and read only that one. So a `w:pict` holding several shapes
kept the first and dropped the rest, and a `v:group` was read as a single
shape -- or, with no text box in it, as nothing at all -- losing every member.
Both are gone after a save, since saving writes the model back as DrawingML.
Read all of them instead:
- `read_vml_all` walks every `v:*` child of a `w:pict` and emits one object
each; `read_vml` keeps returning the first for the two places that show a
single drawing (an OLE object's picture, a `mc:Fallback` preview).
- a `v:group` becomes an `InlineObject::Group`, its members mapped from the
space `coordsize`/`coordorigin` give it onto the group's own box, nested
groups flattened into it -- what `group_members` already does for a
DrawingML `wpg:wgp`, so the model, the renderer and the writer need no
change. Nesting is bounded at `MAX_VML_GROUP_DEPTH`, members at
`MAX_GROUP_CHILDREN`.
- `read_vml_shape` is the old per-shape body, taking the group's scale so a
member's `width`/`height` (and a `v:line`'s `from`/`to`) come through in
the group's coordinates.
`boxes()` in the text-box tests now asks for `text_boxes()` rather than
`text_box()`, which only reports an object that is itself a box and so could
not see one inside a group; its assertions are unchanged.
* DOCX: cover a `v:group` holding several text boxes (#1643)
@KNUFFIXX reported the same loss from the other end: an inline `v:group` with
two `_x0000_t202` boxes side by side came through a convert as box A only, in
both a tall-B and a too-short-B variant, against Word reading both.
A text box is the one member a group used to keep -- `vml_textbox` recurses to
the first box that has content -- so this case looked half-working rather than
broken, and it is worth pinning separately from the shape cases. On `main` the
group collapses to a single `Shape { kind: TextBox, w: 400, h: 100 }` wearing
the group's own size and holding box A's story; here both boxes read, keep
their own stories and positions, and survive a save.
Built in code from the reported fixture rather than committing the .docx.
* DOCX: cover the shapetype lookup for a group's member (#1643)
Word writes each `v:shapetype` once, at its first use, as a child of the
`w:pict` -- so a group's member refers to one that is its parent's sibling,
not its own. Reading all shapes means `read_vml_shape` is now called for a
member as well as for a top-level shape, and both need the picture to find it,
which is why `pict` is threaded through the group walk.
Two tests: a member with no geometry and no colours of its own, taking its
diamond from the shapetype's `o:spt` and its fill from its `fillcolor`, and the
same for a shape outside a group, which is the case the lookup was written for.
* Theme: default to system appearance
Sets Appearance::System as the default variant for ThemePreference,
so fresh installs and unconfigured preference files follow the host
OS appearance dynamically.
* contributors: add the missing blank line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: drop the duplicate entry (already listed on main)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Web prefs test: junk storage keeps the new default (System) appearance
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>
Dialogs, the Read Aloud player and the file browser used egui's small close glyph: the hit area was the glyph alone and hovering only changed its stroke. The close control is now a box at the right end of the title bar, the full title-bar height and 1.6 times as wide, as a Windows caption button on a Word dialog. The whole box closes the window. It fills red with a white cross while the pointer is over it and darker red while pressed and still under the pointer, with colours from the theme tokens in both themes. The box is pointer-only, so Tab never lands on it; dragging from it does not move the window and Escape closes dialogs as before. Its accessibility name is translated like its tooltip.
docs/parity-checklist.md is the regenerated checklist.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A bullet stored as a private-use code (U+F0xx) in a font that isn't
installed, and that isn't a mapped Symbol/Wingdings code, fell back to
any installed face with a glyph at that code - an icon font such as
Font Awesome / Nerd Fonts, where U+F081 is the Twitter logo. Such a
bullet now shows the level's default bullet (• ○ ▪), so level 2 is a
hollow circle.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The web shell never loaded or saved the UiState the desktop app keeps in
ui.json, so a reload reset the theme and every other preference. It now
restores it from localStorage at startup and writes it back whenever it
changes (a reload runs no exit code).
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The font box (ribbon and mini toolbar) opens with a Recently Used Fonts
section: the fonts applied from the box, the mini toolbar or the Font
dialog, most recent first, once each, ten at most, kept in ui.json. All
Fonts follows. The sections show while nothing is typed; a query lists
its matches only. Up/Down walk the recent entries then the list and Enter
applies the highlighted one; a click on a heading keeps the list open.
The typed list is sized again when its length changes, in a discarded
pass so it never blanks; it used to stay one row tall after a one-match
query. Names over 128 bytes are not kept, and reading a saved list stops
at ten names.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(engine): support nested quotations and opening delimiters in smart quotes
* docs: typing-parity line for smart quotes after guillemets, low quotes and nesting
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>
A paragraph in a Thai font that isn't installed (TH Sarabun New, Angsana
New, Cordia New, the UPC families…) drew its Thai letters in one fallback
face and the Latin letters, digits and baht sign in a Latin substitute at
a visibly different size. Word substitutes one font (Cordia New) for the
whole run.
Thai families now have their own substitute lists (Cordia New and
Leelawadee UI on Windows, Thonburi on macOS, Noto Sans Thai and the TLWG
fonts on Linux; serif families prefer Angsana New, Sathu or Noto Serif
Thai), and the first listed face with Latin letters and digits as well
as Thai wins. When the substitute still lacks some of the run's Thai or
Latin text, the run moves to one face covering it through
FontDb::fallback_for_run, the generalised East Asian run fallback. Only
Thai fonts get this: a missing Latin font keeps Latin text in its own
substitute, a run with its own complex-script font keeps it for the Thai
letters, and a missing roman-classed Thai font is no longer drawn in
Cambria.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(doc): preserve overparen, underparen, and brackets in LaTeX group characters
* cargo fmt
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>
* fix(engine): resolve section and section pages context when unlinking fields
* cargo fmt
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>
* fix(engine): support switches and quoted bookmarks when updating REF and PAGEREF fields
* cargo fmt
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>
* Backspace and Delete remove a section break, imported or not (fixes#2127)
* Delete removes a section break on the last paragraph too (#2127)
A continuous break on the mark of the document's last paragraph, with the
last section holding no paragraph (as in the file attached to #2127), had
nothing for Delete to join, so it stayed. Delete at the end now removes it.
The text takes the last section's setup, as in Word, and the headers and
footers that section showed by linking to the removed one stay, here and
when the break goes by joining two paragraphs. Undo brings it back.
* Deleting over several section breaks keeps the headers they linked to (#2127)
A range across two or more breaks dropped the paragraphs in between with
their sections, so only the first section's headers and footers were carried
over. The joined section now takes those it leaves out from every removed
section, nearest first, as "link to previous" resolved them before the delete.
The typing-parity row says that Delete with Track Changes on still leaves a
break on the last paragraph alone.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(engine): use table_formula parse_number for table sorting
* contributors: drop the duplicate entry (already listed)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* cargo fmt
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>